Compare commits

...

15 Commits

Author SHA1 Message Date
centdix
93ffdabd71 rollback to old parsing 2025-08-29 14:59:13 +00:00
centdix
9632f65782 better error handling + cleaning 2025-08-29 14:40:37 +00:00
centdix
092541a744 cleaning 2025-08-29 14:03:25 +00:00
centdix
9fd5b9e00d fix anthropic base url 2025-08-29 14:02:38 +00:00
centdix
901cb716a5 update openai sdk + use stream method 2025-08-29 13:27:00 +00:00
centdix
bd8b708761 cleaning 2025-08-29 12:34:10 +00:00
centdix
97dabd3f1d draft parser 2025-08-29 11:32:58 +00:00
centdix
5e96efdccc draft 2025-08-29 11:10:06 +00:00
centdix
1881ad44cd use anthropic sdk 2025-08-29 09:56:58 +00:00
centdix
07e4e0dc9f nit 2025-08-28 15:22:42 +00:00
centdix
0bbb368c3f better typing 2025-08-28 15:07:27 +00:00
centdix
79cf5dbf68 cleaning 2025-08-28 14:37:27 +00:00
centdix
53b2298233 add anthropic.ts 2025-08-28 14:17:20 +00:00
centdix
0ba58c188f add caching 2025-08-28 13:28:16 +00:00
centdix
c7f487e1a1 working draft 2025-08-28 13:10:29 +00:00
9 changed files with 498 additions and 296 deletions

View File

@@ -156,6 +156,7 @@ impl AIRequestConfig {
let is_azure = matches!(provider, AIProvider::OpenAI) && base_url != OPENAI_BASE_URL
|| matches!(provider, AIProvider::AzureOpenAI);
let is_anthropic = matches!(provider, AIProvider::Anthropic);
let is_anthropic_sdk = headers.get("X-Anthropic-SDK").is_some();
let url = if is_azure && method != Method::GET {
if base_url.ends_with("/deployments") {
@@ -167,6 +168,9 @@ impl AIRequestConfig {
} else {
format!("{}/{}", base_url, path)
}
} else if is_anthropic_sdk {
let truncated_base_url = base_url.trim_end_matches("/v1");
format!("{}/{}", truncated_base_url, path)
} else {
format!("{}/{}", base_url, path)
};

View File

@@ -10,6 +10,7 @@
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.60.0",
"@aws-crypto/sha256-js": "^4.0.0",
"@codingame/monaco-vscode-configuration-service-override": "~20.2.1",
"@codingame/monaco-vscode-editor-api": "~20.2.1",
@@ -56,7 +57,7 @@
"monaco-languageclient": "9.11.0",
"monaco-vim": "^0.4.1",
"ol": "^7.4.0",
"openai": "^4.87.1",
"openai": "^5.16.0",
"openapi-types": "^12.1.3",
"p-limit": "^6.1.0",
"panzoom": "^9.4.3",
@@ -186,6 +187,15 @@
"node": ">=6.0.0"
}
},
"node_modules/@anthropic-ai/sdk": {
"version": "0.60.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.60.0.tgz",
"integrity": "sha512-9zu/TXaUy8BZhXedDtt1wT3H4LOlpKDO1/ftiFpeR3N1PCr3KJFKkxxlQWWt1NNp08xSwUNJ3JNY8yhl8av6eQ==",
"license": "MIT",
"bin": {
"anthropic-ai-sdk": "bin/cli"
}
},
"node_modules/@apidevtools/json-schema-ref-parser": {
"version": "11.6.1",
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.6.1.tgz",
@@ -3640,20 +3650,12 @@
"version": "20.19.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.10.tgz",
"integrity": "sha512-iAFpG6DokED3roLSP0K+ybeDdIX6Bc0Vd3mLW5uDqThPWtNos3E+EqOM11mPQHKzfWHqEBuLjIlsBQQ8CsISmQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/node-fetch": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz",
"integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
"dependencies": {
"@types/node": "*",
"form-data": "^4.0.0"
}
},
"node_modules/@types/normalize-package-data": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
@@ -3953,17 +3955,6 @@
"svelte": "^3.57.0 || ^4.0.0 || ^5.0.0"
}
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/abstract-leveldown": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz",
@@ -4028,17 +4019,6 @@
"ag-grid-community": "31.3.4"
}
},
"node_modules/agentkeepalive": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
"integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
"dependencies": {
"humanize-ms": "^1.2.1"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -4210,7 +4190,8 @@
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true
},
"node_modules/autoprefixer": {
"version": "10.4.21",
@@ -4802,6 +4783,7 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
@@ -5426,6 +5408,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
@@ -5742,6 +5725,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
@@ -6171,14 +6155,6 @@
"node": ">=0.10.0"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/eventemitter3": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
@@ -6405,6 +6381,7 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@@ -6415,23 +6392,6 @@
"node": ">= 6"
}
},
"node_modules/form-data-encoder": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="
},
"node_modules/formdata-node": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
"dependencies": {
"node-domexception": "1.0.0",
"web-streams-polyfill": "4.0.0-beta.3"
},
"engines": {
"node": ">= 12.20"
}
},
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -7129,14 +7089,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
"dependencies": {
"ms": "^2.0.0"
}
},
"node_modules/idb": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz",
@@ -8784,6 +8736,7 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
@@ -8792,6 +8745,7 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"dependencies": {
"mime-db": "1.52.0"
},
@@ -9141,44 +9095,6 @@
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"optional": true
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"deprecated": "Use your platform's native DOMException instead",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-fetch-native": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.6.tgz",
@@ -9363,18 +9279,10 @@
}
},
"node_modules/openai": {
"version": "4.100.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.100.0.tgz",
"integrity": "sha512-9soq/wukv3utxcuD7TWFqKdKp0INWdeyhUCvxwrne5KwnxaCp4eHL4GdT/tMFhYolxgNhxFzg5GFwM331Z5CZg==",
"dependencies": {
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
"abort-controller": "^3.0.0",
"agentkeepalive": "^4.2.1",
"form-data-encoder": "1.7.2",
"formdata-node": "^4.3.2",
"node-fetch": "^2.6.7"
},
"version": "5.16.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-5.16.0.tgz",
"integrity": "sha512-hoEH8ZNvg1HXjU9mp88L/ZH8O082Z8r6FHCXGiWAzVRrEv443aI57qhch4snu07yQydj+AUAWLenAiBXhu89Tw==",
"license": "Apache-2.0",
"bin": {
"openai": "bin/cli"
},
@@ -9391,19 +9299,6 @@
}
}
},
"node_modules/openai/node_modules/@types/node": {
"version": "18.19.101",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.101.tgz",
"integrity": "sha512-Ykg7fcE3+cOQlLUv2Ds3zil6DVjriGQaSN/kEpl5HQ3DIGM6W0F2n9+GkWV4bRt7KjLymgzNdTnSKCbFUUJ7Kw==",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/openai/node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"node_modules/openapi-types": {
"version": "12.1.3",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
@@ -12381,11 +12276,6 @@
"node": ">=6"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@@ -12521,6 +12411,7 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/unified": {
@@ -12956,33 +12847,11 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/web-streams-polyfill": {
"version": "4.0.0-beta.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
"engines": {
"node": ">= 14"
}
},
"node_modules/web-worker": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz",
"integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw=="
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/wheel": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wheel/-/wheel-1.0.0.tgz",

View File

@@ -77,6 +77,7 @@
},
"type": "module",
"dependencies": {
"@anthropic-ai/sdk": "^0.60.0",
"@aws-crypto/sha256-js": "^4.0.0",
"@codingame/monaco-vscode-configuration-service-override": "~20.2.1",
"@codingame/monaco-vscode-editor-api": "~20.2.1",
@@ -123,7 +124,7 @@
"monaco-languageclient": "9.11.0",
"monaco-vim": "^0.4.1",
"ol": "^7.4.0",
"openai": "^4.87.1",
"openai": "^5.16.0",
"openapi-types": "^12.1.3",
"p-limit": "^6.1.0",
"panzoom": "^9.4.3",
@@ -544,4 +545,4 @@
"@rollup/rollup-linux-x64-gnu": "^4.35.0",
"fsevents": "^2.3.3"
}
}
}

View File

@@ -11,16 +11,13 @@ import HistoryManager from './HistoryManager.svelte'
import {
extractCodeFromMarkdown,
getLatestAssistantMessage,
processToolCall,
type DisplayMessage,
type Tool,
type ToolCallbacks,
type ToolDisplayMessage
} from './shared'
import type {
ChatCompletionChunk,
ChatCompletionMessageParam,
ChatCompletionMessageToolCall,
ChatCompletionSystemMessageParam,
ChatCompletionUserMessageParam
} from 'openai/resources/chat/completions.mjs'
@@ -34,7 +31,7 @@ import { loadApiTools } from './api/apiTools'
import { prepareScriptUserMessage } from './script/core'
import { prepareNavigatorUserMessage } from './navigator/core'
import { sendUserToast } from '$lib/toast'
import { getCompletion, getModelContextWindow } from '../lib'
import { getCompletion, getModelContextWindow, parseOpenAICompletion } from '../lib'
import { dfs } from '$lib/components/flows/previousResults'
import { getStringError } from './utils'
import type { FlowModuleState, FlowState } from '$lib/components/flows/flowState'
@@ -47,6 +44,7 @@ import type { ContextElement } from './context'
import type { Selection } from 'monaco-editor'
import type AIChatInput from './AIChatInput.svelte'
import { prepareApiSystemMessage, prepareApiUserMessage } from './api/core'
import { getAnthropicCompletion, parseAnthropicCompletion } from './anthropic'
// If the estimated token usage is greater than the model context window - the threshold, we delete the oldest message
const MAX_TOKENS_THRESHOLD_PERCENTAGE = 0.05
@@ -380,10 +378,8 @@ class AIChatManager {
}
systemMessage?: ChatCompletionSystemMessageParam
}) => {
let addedMessages: ChatCompletionMessageParam[] = []
try {
let completion: any = null
let addedMessages: ChatCompletionMessageParam[] = []
while (true) {
const systemMessage = systemMessageOverride ?? this.systemMessage
const helpers = this.helpers
@@ -413,99 +409,28 @@ class AIChatManager {
}
this.pendingPrompt = ''
}
completion = await getCompletion(
const model = getCurrentModel()
const completionFn = model.provider === 'anthropic' ? getAnthropicCompletion : getCompletion
const parseFn =
model.provider === 'anthropic' ? parseAnthropicCompletion : parseOpenAICompletion
const completion = await completionFn(
[systemMessage, ...messages, ...(pendingUserMessage ? [pendingUserMessage] : [])],
abortController,
tools.map((t) => t.def)
)
if (completion) {
const finalToolCalls: Record<number, ChatCompletionChunk.Choice.Delta.ToolCall> = {}
let answer = ''
for await (const chunk of completion) {
if (!('choices' in chunk && chunk.choices.length > 0 && 'delta' in chunk.choices[0])) {
continue
}
const c = chunk as ChatCompletionChunk
const delta = c.choices[0].delta.content
if (delta) {
answer += delta
callbacks.onNewToken(delta)
}
const toolCalls = c.choices[0].delta.tool_calls || []
if (toolCalls.length > 0 && answer) {
// if tool calls are present but we have some textual content already, we need to display it to the user first
callbacks.onMessageEnd()
answer = ''
}
for (const toolCall of toolCalls) {
const { index } = toolCall
let finalToolCall = finalToolCalls[index]
if (!finalToolCall) {
finalToolCalls[index] = toolCall
} else {
if (toolCall.function?.arguments) {
if (!finalToolCall.function) {
finalToolCall.function = toolCall.function
} else {
finalToolCall.function.arguments =
(finalToolCall.function.arguments ?? '') + toolCall.function.arguments
}
}
}
finalToolCall = finalToolCalls[index]
if (finalToolCall?.function) {
const {
function: { name: funcName },
id: toolCallId
} = finalToolCall
if (funcName && toolCallId) {
const tool = tools.find((t) => t.def.function.name === funcName)
if (tool && tool.preAction) {
tool.preAction({ toolCallbacks: callbacks, toolId: toolCallId })
}
}
}
}
}
if (answer) {
const toAdd = { role: 'assistant' as const, content: answer }
addedMessages.push(toAdd)
messages.push(toAdd)
}
callbacks.onMessageEnd()
const toolCalls = Object.values(finalToolCalls).filter(
(toolCall) => toolCall.id !== undefined && toolCall.function?.arguments !== undefined
) as ChatCompletionMessageToolCall[]
if (toolCalls.length > 0) {
const toAdd = {
role: 'assistant' as const,
tool_calls: toolCalls.map((t) => ({
...t,
function: {
...t.function,
arguments: t.function.arguments || '{}'
}
}))
}
messages.push(toAdd)
addedMessages.push(toAdd)
for (const toolCall of toolCalls) {
const messageToAdd = await processToolCall({
tools,
toolCall,
helpers,
toolCallbacks: callbacks
})
messages.push(messageToAdd)
addedMessages.push(messageToAdd)
}
} else {
const continueCompletion = await parseFn(
completion as any,
callbacks,
messages,
addedMessages,
tools,
helpers
)
if (!continueCompletion) {
break
}
}

View File

@@ -0,0 +1,264 @@
import { OpenAI } from 'openai'
import type {
ChatCompletionMessageParam,
ChatCompletionMessageFunctionToolCall
} from 'openai/resources/index.mjs'
import type {
MessageParam,
TextBlockParam,
ToolUnion,
ToolUseBlockParam,
Tool as AnthropicTool,
Message
} from '@anthropic-ai/sdk/resources'
import type { MessageStream } from '@anthropic-ai/sdk/lib/MessageStream'
import { getProviderAndCompletionConfig, workspaceAIClients } from '../lib'
import { processToolCall, type Tool, type ToolCallbacks } from './shared'
export async function getAnthropicCompletion(
messages: ChatCompletionMessageParam[],
abortController: AbortController,
tools?: OpenAI.Chat.Completions.ChatCompletionFunctionTool[]
): Promise<MessageStream> {
const { provider, config } = getProviderAndCompletionConfig({ messages, stream: true })
const { system, messages: anthropicMessages } = convertOpenAIToAnthropicMessages(messages)
const anthropicTools = convertOpenAIToolsToAnthropic(tools)
const anthropicClient = workspaceAIClients.getAnthropicClient()
const anthropicParams = {
model: config.model,
max_tokens: config.max_tokens as number,
messages: anthropicMessages,
...(system && { system }),
...(anthropicTools && { tools: anthropicTools }),
...(typeof config.temperature === 'number' && { temperature: config.temperature })
}
const stream = anthropicClient.messages.stream(anthropicParams, {
signal: abortController.signal,
headers: {
'X-Provider': provider,
'anthropic-version': '2023-06-01',
'X-Anthropic-SDK': 'true'
}
})
return stream
}
export async function parseAnthropicCompletion(
completion: MessageStream,
callbacks: ToolCallbacks & {
onNewToken: (token: string) => void
onMessageEnd: () => void
},
messages: ChatCompletionMessageParam[],
addedMessages: ChatCompletionMessageParam[],
tools: Tool<any>[],
helpers: any
): Promise<boolean> {
let toolCallsToProcess: ChatCompletionMessageFunctionToolCall[] = []
let error = null
// Handle text streaming
completion.on('text', (textDelta: string, _textSnapshot: string) => {
callbacks.onNewToken(textDelta)
})
completion.on('message', (message: Message) => {
for (const block of message.content) {
if (block.type === 'text') {
const text = block.text
const assistantMessage = { role: 'assistant' as const, content: text }
messages.push(assistantMessage)
addedMessages.push(assistantMessage)
callbacks.onMessageEnd()
} else if (block.type === 'tool_use') {
// Convert Anthropic tool calls to OpenAI format for compatibility
toolCallsToProcess.push({
id: block.id,
type: 'function' as const,
function: {
name: block.name,
arguments: JSON.stringify(block.input)
}
})
// Preprocess tool if it has a preAction
const tool = tools.find((t) => t.def.function.name === block.name)
if (tool && tool.preAction) {
tool.preAction({ toolCallbacks: callbacks, toolId: block.id })
}
}
}
})
// Handle errors
completion.on('error', (error: any) => {
console.error('Anthropic stream error:', error)
error = error
})
// Wait for completion
await completion.done()
callbacks.onMessageEnd()
if (error) {
throw error
}
// Process tool calls if any
if (toolCallsToProcess.length > 0) {
const assistantWithTools = {
role: 'assistant' as const,
tool_calls: toolCallsToProcess
}
messages.push(assistantWithTools)
addedMessages.push(assistantWithTools)
// Process each tool call
for (const toolCall of toolCallsToProcess) {
const messageToAdd = await processToolCall({
tools,
toolCall,
helpers,
toolCallbacks: callbacks
})
messages.push(messageToAdd)
addedMessages.push(messageToAdd)
}
return true // Continue the conversation loop
}
return false // End the conversation
}
export function convertOpenAIToAnthropicMessages(messages: ChatCompletionMessageParam[]): {
system: TextBlockParam[] | undefined
messages: MessageParam[]
} {
let system: TextBlockParam[] | undefined
const anthropicMessages: MessageParam[] = []
for (const message of messages) {
if (message.role === 'system') {
const systemText =
typeof message.content === 'string' ? message.content : JSON.stringify(message.content)
// Convert system to array format with cache_control for caching
system = [
{
type: 'text',
text: systemText,
cache_control: { type: 'ephemeral' }
}
]
continue
}
if (message.role === 'user') {
anthropicMessages.push({
role: 'user',
content:
typeof message.content === 'string' ? message.content : JSON.stringify(message.content)
})
} else if (message.role === 'assistant') {
const content: (TextBlockParam | ToolUseBlockParam)[] = []
if (message.content) {
content.push({
type: 'text',
text:
typeof message.content === 'string' ? message.content : JSON.stringify(message.content)
})
}
if (message.tool_calls) {
for (const toolCall of message.tool_calls) {
if (toolCall.type !== 'function') continue
let input = {}
try {
input = JSON.parse(toolCall.function.arguments || '{}')
} catch (e) {
console.error('Failed to parse tool call arguments', e)
}
content.push({
type: 'tool_use',
id: toolCall.id,
name: toolCall.function.name,
input
})
}
}
if (content.length > 0) {
anthropicMessages.push({
role: 'assistant',
content: content.length === 1 && content[0].type === 'text' ? content[0].text : content
})
}
} else if (message.role === 'tool') {
// Tool results must be in user messages in Anthropic format
anthropicMessages.push({
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: message.tool_call_id,
content:
typeof message.content === 'string'
? message.content
: JSON.stringify(message.content)
}
]
})
}
}
// Add cache_control to the last message content blocks
if (anthropicMessages.length > 0) {
const lastMessage = anthropicMessages[anthropicMessages.length - 1]
if (Array.isArray(lastMessage.content)) {
// Add cache_control to the last content block
if (lastMessage.content.length > 0) {
const lastBlock = lastMessage.content[lastMessage.content.length - 1]
if (lastBlock.type === 'text') {
lastBlock.cache_control = { type: 'ephemeral' }
}
}
} else if (typeof lastMessage.content === 'string') {
// Convert string content to array format with cache_control
lastMessage.content = [
{
type: 'text',
text: lastMessage.content,
cache_control: { type: 'ephemeral' }
}
]
}
}
return { system, messages: anthropicMessages }
}
export function convertOpenAIToolsToAnthropic(
tools?: OpenAI.Chat.Completions.ChatCompletionFunctionTool[]
): ToolUnion[] | undefined {
if (!tools || tools.length === 0) return undefined
const anthropicTools: ToolUnion[] = tools.map((tool) => ({
name: tool.function.name,
description: tool.function.description,
input_schema: (tool.function.parameters || {
type: 'object',
properties: {}
}) as AnthropicTool.InputSchema
}))
// Add cache_control to the last tool to cache all tool definitions
if (anthropicTools.length > 0) {
anthropicTools[anthropicTools.length - 1].cache_control = { type: 'ephemeral' }
}
return anthropicTools
}

View File

@@ -1,11 +1,11 @@
import type { ChatCompletionTool } from 'openai/resources/index.mjs'
import type { ChatCompletionFunctionTool } from 'openai/resources/index.mjs'
import type { Tool } from '../shared'
import { get } from 'svelte/store'
import { workspaceStore } from '$lib/stores'
import type { EndpointTool } from '$lib/gen/types.gen'
import { McpService } from '$lib/gen/services.gen'
function buildApiCallTool(endpointTool: EndpointTool): ChatCompletionTool {
function buildApiCallTool(endpointTool: EndpointTool): ChatCompletionFunctionTool {
// Build the parameters schema for OpenAI function calling
const parameters: Record<string, any> = {
type: 'object',
@@ -18,10 +18,13 @@ function buildApiCallTool(endpointTool: EndpointTool): ChatCompletionTool {
for (const [key, schema] of Object.entries(endpointTool.path_params_schema.properties)) {
// Skip workspace parameter as it's auto-filled
if (key === 'workspace') continue
parameters.properties[key] = schema
if (Array.isArray(endpointTool.path_params_schema.required) && endpointTool.path_params_schema.required.includes(key)) {
if (
Array.isArray(endpointTool.path_params_schema.required) &&
endpointTool.path_params_schema.required.includes(key)
) {
parameters.required.push(key)
}
}
@@ -31,8 +34,11 @@ function buildApiCallTool(endpointTool: EndpointTool): ChatCompletionTool {
if (endpointTool.query_params_schema?.properties) {
for (const [key, schema] of Object.entries(endpointTool.query_params_schema.properties)) {
parameters.properties[key] = schema
if (Array.isArray(endpointTool.query_params_schema.required) && endpointTool.query_params_schema.required.includes(key)) {
if (
Array.isArray(endpointTool.query_params_schema.required) &&
endpointTool.query_params_schema.required.includes(key)
) {
parameters.required.push(key)
}
}
@@ -47,8 +53,11 @@ function buildApiCallTool(endpointTool: EndpointTool): ChatCompletionTool {
properties: endpointTool.body_schema.properties,
required: endpointTool.body_schema.required || []
}
if (Array.isArray(endpointTool.body_schema.required) && endpointTool.body_schema.required.length > 0) {
if (
Array.isArray(endpointTool.body_schema.required) &&
endpointTool.body_schema.required.length > 0
) {
parameters.required.push('body')
}
}
@@ -63,16 +72,17 @@ function buildApiCallTool(endpointTool: EndpointTool): ChatCompletionTool {
}
}
function buildToolsFromEndpoints(
endpointTools: EndpointTool[]
): { tools: ChatCompletionTool[]; endpointMap: Record<string, { method: string; path: string }> } {
const tools: ChatCompletionTool[] = []
function buildToolsFromEndpoints(endpointTools: EndpointTool[]): {
tools: ChatCompletionFunctionTool[]
endpointMap: Record<string, { method: string; path: string }>
} {
const tools: ChatCompletionFunctionTool[] = []
const endpointMap: Record<string, { method: string; path: string }> = {}
for (const endpointTool of endpointTools) {
const tool = buildApiCallTool(endpointTool)
tools.push(tool)
// Store the endpoint info in the map
endpointMap[endpointTool.name] = {
method: endpointTool.method,
@@ -84,17 +94,17 @@ function buildToolsFromEndpoints(
}
export function createApiTools(
chatTools: ChatCompletionTool[],
chatTools: ChatCompletionFunctionTool[],
endpointMap: Record<string, { method: string; path: string }> = {}
): Tool<{}>[] {
return chatTools.map((chatTool) => {
const toolName = chatTool.function.name
const endpoint = endpointMap[toolName]
const method = endpoint?.method?.toUpperCase() || 'GET'
// Determine if tool needs confirmation based on method
const needsConfirmation = ['DELETE', 'POST', 'PUT', 'PATCH'].includes(method)
return {
def: chatTool,
requiresConfirmation: needsConfirmation,
@@ -103,7 +113,7 @@ export function createApiTools(
fn: async ({ args, toolId, toolCallbacks }) => {
const toolName = chatTool.function.name
const endpoint = endpointMap[toolName]
if (!endpoint) {
throw new Error(`No endpoint mapping found for tool ${toolName}`)
}
@@ -111,7 +121,7 @@ export function createApiTools(
try {
const workspace = get(workspaceStore) as string
let path = endpoint.path.replace('{workspace}', workspace)
// Build URL with path parameters
let url = `/api${path}`
const queryParams: Record<string, string> = {}
@@ -143,7 +153,7 @@ export function createApiTools(
}
toolCallbacks.setToolStatus(toolId, {
content: `Calling ${toolName}...`,
content: `Calling ${toolName}...`
})
const fetchOptions: RequestInit = {
@@ -173,7 +183,7 @@ export function createApiTools(
})
toolCallbacks.setToolStatus(toolId, {
content: `Call to ${toolName} completed`,
result: jsonResult,
result: jsonResult
})
return jsonResult
} else {
@@ -186,7 +196,7 @@ export function createApiTools(
toolCallbacks.setToolStatus(toolId, {
content: `Call to ${toolName} failed`,
result: jsonResult,
error: `HTTP ${response.status}: ${text}`,
error: `HTTP ${response.status}: ${text}`
})
return jsonResult
}
@@ -194,7 +204,7 @@ export function createApiTools(
const errorMessage = `Error calling API: ${error instanceof Error ? error.message : String(error)}`
toolCallbacks.setToolStatus(toolId, {
content: `Call to ${toolName} failed`,
error: errorMessage,
error: errorMessage
})
console.error(`Error calling API:`, error)
return errorMessage
@@ -210,10 +220,10 @@ export async function loadApiTools(): Promise<Tool<{}>[]> {
const endpointTools = await McpService.listMcpTools({
workspace: get(workspaceStore) as string
})
// Build tools from the endpoint definitions
const { tools: apiTools, endpointMap } = buildToolsFromEndpoints(endpointTools)
// Create executable tools
const executableApiTools = createApiTools(apiTools, endpointMap)
return executableApiTools
@@ -221,4 +231,4 @@ export async function loadApiTools(): Promise<Tool<{}>[]> {
console.error('Failed to load API tools:', error)
return []
}
}
}

View File

@@ -5,7 +5,7 @@ import { get } from 'svelte/store'
import { compile, phpCompile, pythonCompile } from '../../utils'
import type {
ChatCompletionSystemMessageParam,
ChatCompletionTool,
ChatCompletionFunctionTool,
ChatCompletionUserMessageParam
} from 'openai/resources/index.mjs'
import { type DBSchema, dbSchemas, getCurrentModel } from '$lib/stores'
@@ -498,7 +498,7 @@ export function prepareScriptUserMessage(
}
}
const RESOURCE_TYPE_FUNCTION_DEF: ChatCompletionTool = {
const RESOURCE_TYPE_FUNCTION_DEF: ChatCompletionFunctionTool = {
type: 'function',
function: {
name: 'search_resource_types',
@@ -519,7 +519,7 @@ const RESOURCE_TYPE_FUNCTION_DEF: ChatCompletionTool = {
}
}
const DB_SCHEMA_FUNCTION_DEF: ChatCompletionTool = {
const DB_SCHEMA_FUNCTION_DEF: ChatCompletionFunctionTool = {
type: 'function',
function: {
name: 'get_db_schema',
@@ -693,7 +693,7 @@ export async function searchExternalIntegrationResources(args: { query: string }
}
}
const SEARCH_NPM_PACKAGES_TOOL: ChatCompletionTool = {
const SEARCH_NPM_PACKAGES_TOOL: ChatCompletionFunctionTool = {
type: 'function',
function: {
name: 'search_npm_packages',
@@ -778,7 +778,7 @@ export async function fetchNpmPackageTypes(
}
}
const TEST_RUN_SCRIPT_TOOL: ChatCompletionTool = {
const TEST_RUN_SCRIPT_TOOL: ChatCompletionFunctionTool = {
type: 'function',
function: {
name: 'test_run_script',

View File

@@ -1,7 +1,7 @@
import type {
ChatCompletionMessageParam,
ChatCompletionMessageToolCall,
ChatCompletionTool
ChatCompletionFunctionTool,
ChatCompletionMessageFunctionToolCall,
ChatCompletionMessageParam
} from 'openai/resources/chat/completions.mjs'
import { get } from 'svelte/store'
import type { CodePieceElement, ContextElement, FlowModuleCodePieceElement } from './context'
@@ -257,7 +257,7 @@ export async function processToolCall<T>({
toolCallbacks
}: {
tools: Tool<T>[]
toolCall: ChatCompletionMessageToolCall
toolCall: ChatCompletionMessageFunctionToolCall
helpers: T
toolCallbacks: ToolCallbacks
}): Promise<ChatCompletionMessageParam> {
@@ -345,7 +345,7 @@ export async function processToolCall<T>({
}
export interface Tool<T> {
def: ChatCompletionTool
def: ChatCompletionFunctionTool
fn: (p: {
args: any
workspace: string
@@ -369,7 +369,7 @@ export function createToolDef(
zodSchema: z.ZodSchema,
name: string,
description: string
): ChatCompletionTool {
): ChatCompletionFunctionTool {
const schema = zodToJsonSchema(zodSchema, {
name,
target: 'openAi'
@@ -438,7 +438,7 @@ export const createSearchHubScriptsTool = (withContent: boolean = false) => ({
})
export async function buildSchemaForTool(
toolDef: ChatCompletionTool,
toolDef: ChatCompletionFunctionTool,
schemaBuilder: () => Promise<FunctionParameters>
): Promise<boolean> {
try {
@@ -586,7 +586,10 @@ function getErrorMessage(result: unknown): string {
}
// Build test run args based on the tool definition, if it contains a fallback schema
export async function buildTestRunArgs(args: any, toolDef: ChatCompletionTool): Promise<any> {
export async function buildTestRunArgs(
args: any,
toolDef: ChatCompletionFunctionTool
): Promise<any> {
let parsedArgs = args
// if the schema is the fallback schema, parse the args as a JSON string
if (

View File

@@ -7,18 +7,23 @@ import {
type SQLSchema
} from '$lib/stores'
import { buildClientSchema, printSchema } from 'graphql'
import { OpenAI } from 'openai'
import OpenAI from 'openai'
import type {
ChatCompletionChunk,
ChatCompletionCreateParams,
ChatCompletionCreateParamsNonStreaming,
ChatCompletionCreateParamsStreaming,
ChatCompletionMessageFunctionToolCall,
ChatCompletionMessageParam
} from 'openai/resources/index.mjs'
import Anthropic from '@anthropic-ai/sdk'
import { get, type Writable } from 'svelte/store'
import { OpenAPI, ResourceService, type Script } from '../../gen'
import { EDIT_CONFIG, FIX_CONFIG, GEN_CONFIG } from './prompts'
import { formatResourceTypes } from './utils'
import { z } from 'zod'
import { processToolCall, type Tool, type ToolCallbacks } from './chat/shared'
import type { Stream } from 'openai/core/streaming.mjs'
export const SUPPORTED_LANGUAGES = new Set(Object.keys(GEN_CONFIG.prompts))
@@ -219,9 +224,11 @@ export const PROVIDER_COMPLETION_CONFIG_MAP: Record<AIProvider, ChatCompletionCr
class WorkspacedAIClients {
private openaiClient: OpenAI | undefined
private anthropicClient: Anthropic | undefined
init(workspace: string) {
this.initOpenai(workspace)
this.initAnthropic(workspace)
}
private getBaseURL(workspace: string) {
@@ -240,12 +247,28 @@ class WorkspacedAIClients {
})
}
private initAnthropic(workspace: string) {
const baseURL = this.getBaseURL(workspace)
this.anthropicClient = new Anthropic({
baseURL,
apiKey: 'fake-key',
dangerouslyAllowBrowser: true
})
}
getOpenaiClient() {
if (!this.openaiClient) {
throw new Error('OpenAI not initialized')
}
return this.openaiClient
}
getAnthropicClient() {
if (!this.anthropicClient) {
throw new Error('Anthropic not initialized')
}
return this.anthropicClient
}
}
export const workspaceAIClients = new WorkspacedAIClients()
@@ -460,7 +483,7 @@ const PROMPTS_CONFIGS = {
gen: GEN_CONFIG
}
function getProviderAndCompletionConfig<K extends boolean>({
export function getProviderAndCompletionConfig<K extends boolean>({
messages,
stream,
tools,
@@ -622,10 +645,11 @@ export async function getCompletion(
messages: ChatCompletionMessageParam[],
abortController: AbortController,
tools?: OpenAI.Chat.Completions.ChatCompletionTool[]
) {
): Promise<Stream<ChatCompletionChunk>> {
const { provider, config } = getProviderAndCompletionConfig({ messages, stream: true, tools })
const openaiClient = workspaceAIClients.getOpenaiClient()
const completion = await openaiClient.chat.completions.create(config, {
const completion = openaiClient.chat.completions.create(config, {
signal: abortController.signal,
headers: {
'X-Provider': provider
@@ -634,6 +658,108 @@ export async function getCompletion(
return completion
}
export async function parseOpenAICompletion(
completion: Stream<ChatCompletionChunk>,
callbacks: ToolCallbacks & {
onNewToken: (token: string) => void
onMessageEnd: () => void
},
messages: ChatCompletionMessageParam[],
addedMessages: ChatCompletionMessageParam[],
tools: Tool<any>[],
helpers: any
): Promise<boolean> {
const finalToolCalls: Record<number, ChatCompletionChunk.Choice.Delta.ToolCall> = {}
let answer = ''
for await (const chunk of completion) {
if (!('choices' in chunk && chunk.choices.length > 0 && 'delta' in chunk.choices[0])) {
continue
}
const c = chunk as ChatCompletionChunk
const delta = c.choices[0].delta.content
if (delta) {
answer += delta
callbacks.onNewToken(delta)
}
const toolCalls = c.choices[0].delta.tool_calls || []
if (toolCalls.length > 0 && answer) {
// if tool calls are present but we have some textual content already, we need to display it to the user first
callbacks.onMessageEnd()
answer = ''
}
for (const toolCall of toolCalls) {
const { index } = toolCall
let finalToolCall = finalToolCalls[index]
if (!finalToolCall) {
finalToolCalls[index] = toolCall
} else {
if (toolCall.function?.arguments) {
if (!finalToolCall.function) {
finalToolCall.function = toolCall.function
} else {
finalToolCall.function.arguments =
(finalToolCall.function.arguments ?? '') + toolCall.function.arguments
}
}
}
finalToolCall = finalToolCalls[index]
if (finalToolCall?.function) {
const {
function: { name: funcName },
id: toolCallId
} = finalToolCall
if (funcName && toolCallId) {
const tool = tools.find((t) => t.def.function.name === funcName)
if (tool && tool.preAction) {
tool.preAction({ toolCallbacks: callbacks, toolId: toolCallId })
}
}
}
}
}
if (answer) {
const toAdd = { role: 'assistant' as const, content: answer }
addedMessages.push(toAdd)
messages.push(toAdd)
}
callbacks.onMessageEnd()
const toolCalls = Object.values(finalToolCalls).filter(
(toolCall) => toolCall.id !== undefined && toolCall.function?.arguments !== undefined
) as ChatCompletionMessageFunctionToolCall[]
if (toolCalls.length > 0) {
const toAdd = {
role: 'assistant' as const,
tool_calls: toolCalls.map((t) => ({
...t,
function: {
...t.function,
arguments: t.function.arguments || '{}'
}
}))
}
messages.push(toAdd)
addedMessages.push(toAdd)
for (const toolCall of toolCalls) {
const messageToAdd = await processToolCall({
tools,
toolCall,
helpers,
toolCallbacks: callbacks
})
messages.push(messageToAdd)
addedMessages.push(messageToAdd)
}
} else {
return false
}
return true
}
export function getResponseFromEvent(part: OpenAI.Chat.Completions.ChatCompletionChunk): string {
return part.choices?.[0]?.delta?.content || ''
}
@@ -757,7 +883,7 @@ export async function deltaCodeCompletion(
}
if (!match[1].endsWith('`')) {
// skip udpating if possible that part of three ticks (end of code block)s
// skip updating if possible that part of three ticks (end of code block)s
delta = getStringEndDelta(code, match[1])
generatedCodeDelta.set(delta)
code = match[1]