From 80748ac01862958a4a9b66e8f060d5aadf2dabf3 Mon Sep 17 00:00:00 2001 From: Xi Lyu Date: Tue, 27 Oct 2026 16:11:54 +0000 Subject: [PATCH 1/6] Add a VS Code extension that serves Databricks models to Copilot Chat A language model chat provider for VS Code (vscode-extension/, TypeScript). It adds the workspace's AI Gateway models to the chat model picker and calls the gateway directly, with no local proxy and no runtime dependencies: Chat Completions (/ai-gateway/mlflow/v1/chat/completions) for Claude and open models, and stateless Responses (/ai-gateway/mlflow/v1/responses) for GPT-5 and later, which reject function tools on Chat Completions. Streaming, tool calls, tool results and images work on both. It reads unityGateway.workspace, unityGateway.models and unityGateway.authCommand from user settings only, so a workspace's settings can't redirect the token or choose the command that prints it. The token comes from running the auth command (ug auth-token), is kept in memory until shortly before it expires, and after a 401 is fetched again with --force-refresh. Nothing is stored. Refresh Models re-reads the settings. The minified bundle and package.json are built into src/ucode/vscode/extension/ and committed, so ug can pack and install the extension without Node; CI fails when they are stale, and the job is part of the required test check. ug configure doesn't install it yet. Co-authored-by: Isaac --- .gitattributes | 1 + .github/workflows/ci.yml | 36 +- justfile | 4 + src/ucode/vscode/extension/out/extension.js | 2 + src/ucode/vscode/extension/package.json | 95 + vscode-extension/.gitignore | 2 + vscode-extension/.npmrc | 3 + vscode-extension/README.md | 22 + vscode-extension/package-lock.json | 1719 +++++++++++++++++++ vscode-extension/package.json | 95 + vscode-extension/src/auth.ts | 73 + vscode-extension/src/extension.ts | 127 ++ vscode-extension/src/formats.ts | 194 +++ vscode-extension/src/gateway.ts | 55 + vscode-extension/test/auth.test.ts | 51 + vscode-extension/test/extension.test.ts | 135 ++ vscode-extension/test/formats.test.ts | 105 ++ vscode-extension/test/support.ts | 54 + vscode-extension/test/vscode.ts | 40 + vscode-extension/tsconfig.json | 16 + vscode-extension/vitest.config.ts | 8 + 21 files changed, 2834 insertions(+), 3 deletions(-) create mode 100644 .gitattributes create mode 100644 src/ucode/vscode/extension/out/extension.js create mode 100644 src/ucode/vscode/extension/package.json create mode 100644 vscode-extension/.gitignore create mode 100644 vscode-extension/.npmrc create mode 100644 vscode-extension/README.md create mode 100644 vscode-extension/package-lock.json create mode 100644 vscode-extension/package.json create mode 100644 vscode-extension/src/auth.ts create mode 100644 vscode-extension/src/extension.ts create mode 100644 vscode-extension/src/formats.ts create mode 100644 vscode-extension/src/gateway.ts create mode 100644 vscode-extension/test/auth.test.ts create mode 100644 vscode-extension/test/extension.test.ts create mode 100644 vscode-extension/test/formats.test.ts create mode 100644 vscode-extension/test/support.ts create mode 100644 vscode-extension/test/vscode.ts create mode 100644 vscode-extension/tsconfig.json create mode 100644 vscode-extension/vitest.config.ts diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..e21d1145a --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +src/ucode/vscode/extension/out/extension.js linguist-generated=true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24dbaab06..af34fb5d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,6 +89,35 @@ jobs: export UCODE_TEST_CLAUDE_BINARY="$(command -v claude)" uv run pytest tests/native_claude/test_mod.py tests/native_claude/test_credentials.py -q + vscode-extension: + name: VS Code extension + runs-on: ubuntu-latest + needs: lint + timeout-minutes: 10 + defaults: + run: + working-directory: vscode-extension + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: vscode-extension/package-lock.json + - run: npm ci --no-audit --no-fund + - run: npm run typecheck + - run: npm test + # ug packs the extension from these committed files, so they must match the sources. + - name: Check the committed bundle is up to date + run: | + npm run build + test -z "$(git status --porcelain -- ../src/ucode/vscode/extension)" || { + echo "::error::The VS Code extension bundle is stale. Run \`just vscode-build\` and commit the result." + exit 1 + } + e2e-shards: name: ${{ matrix.name }} ยท ${{ matrix.os == 'windows-latest' && 'Windows' || 'Linux' }} runs-on: ${{ matrix.os }} @@ -200,14 +229,15 @@ jobs: required-tests: name: test if: ${{ !cancelled() }} - needs: test + needs: [test, vscode-extension] runs-on: ubuntu-latest timeout-minutes: 5 steps: - - name: Require unit tests to pass + - name: Require unit and VS Code extension tests to pass env: RESULT: ${{ needs.test.result }} - run: test "$RESULT" = success + VSCODE_EXTENSION_RESULT: ${{ needs.vscode-extension.result }} + run: test "$RESULT" = success && test "$VSCODE_EXTENSION_RESULT" = success required-e2e: name: e2e diff --git a/justfile b/justfile index f1fb8558f..faa6c7098 100644 --- a/justfile +++ b/justfile @@ -26,3 +26,7 @@ ty: fix: uv run ruff check --fix src/ tests/ uv run ruff format src/ tests/ + +# Rebuild the VS Code extension bundle ug packs at configure time. Commit the result. +vscode-build: + cd vscode-extension && npm ci --no-audit --no-fund && npm run build diff --git a/src/ucode/vscode/extension/out/extension.js b/src/ucode/vscode/extension/out/extension.js new file mode 100644 index 000000000..91bf51fda --- /dev/null +++ b/src/ucode/vscode/extension/out/extension.js @@ -0,0 +1,2 @@ +"use strict";var J=Object.create;var f=Object.defineProperty;var U=Object.getOwnPropertyDescriptor;var F=Object.getOwnPropertyNames;var B=Object.getPrototypeOf,W=Object.prototype.hasOwnProperty;var H=(t,n)=>{for(var e in n)f(t,e,{get:n[e],enumerable:!0})},C=(t,n,e,s)=>{if(n&&typeof n=="object"||typeof n=="function")for(let a of F(n))!W.call(t,a)&&a!==e&&f(t,a,{get:()=>n[a],enumerable:!(s=U(n,a))||s.enumerable});return t};var x=(t,n,e)=>(e=t!=null?J(B(t)):{},C(n||!t||!t.__esModule?f(e,"default",{value:t,enumerable:!0}):e,t)),K=t=>C(f({},"__esModule",{value:!0}),t);var te={};H(te,{DatabricksModels:()=>v,activate:()=>ee,readSettings:()=>N});module.exports=K(te);var c=x(require("vscode"));var b={name:"unity-gateway",displayName:"Databricks Unity Gateway",description:"Chat with Databricks AI Gateway models in VS Code through Unity Gateway (ug)",version:"0.1.0",publisher:"databricks",repository:{type:"git",url:"https://github.com/databricks/unity-gateway.git",directory:"vscode-extension"},engines:{vscode:"^1.128.0"},categories:["AI","Chat"],extensionKind:["ui"],main:"./out/extension.js",activationEvents:[],contributes:{languageModelChatProviders:[{vendor:"databricks-unity-gateway",displayName:"Databricks"}],configuration:{title:"Databricks Unity Gateway",properties:{"unityGateway.workspace":{type:"string",default:"",description:"Databricks workspace URL, for example https://my-workspace.cloud.databricks.com. Set by `ug configure`.",scope:"application"},"unityGateway.models":{type:"array",default:[],description:"Models to offer in the chat model picker. Set by `ug configure`.",items:{type:"object",required:["id","api"],properties:{id:{type:"string",description:"Model service name, for example system.ai.claude-sonnet-5-5."},api:{type:"string",enum:["chat_completions","responses"],description:"The AI Gateway API the model is called through."}}},scope:"application"},"unityGateway.authCommand":{type:"string",default:"",description:"Command that prints a Databricks access token. Set by `ug configure`.",scope:"application"}}},commands:[{command:"unityGateway.refreshModels",title:"Refresh Models",category:"Unity Gateway"}]},scripts:{build:`esbuild src/extension.ts --bundle --minify --legal-comments=none --outfile=../src/ucode/vscode/extension/out/extension.js --external:vscode --format=cjs --platform=node --target=node22 && node -e "require('fs').copyFileSync('package.json', '../src/ucode/vscode/extension/package.json')"`,typecheck:"tsc",test:"vitest run"},devDependencies:{"@types/node":"^22.20.3","@types/vscode":"1.125.0",esbuild:"^0.28.2",typescript:"^5.9.3",vitest:"^4.1.12"}};var T=require("node:child_process"),z=5*6e4,V=60*6e4,X=t=>new Promise((n,e)=>{(0,T.exec)(t,{timeout:6e4,windowsHide:!0},(s,a,o)=>{s?e(new Error(`Couldn't get a Databricks token: \`${t}\` failed: ${String(o).trim().slice(0,500)||s.message}`)):n(String(a).trim())})}),Q=t=>{try{let{exp:n}=JSON.parse(Buffer.from(t.split(".")[1]??"","base64url").toString());if(typeof n=="number")return n*1e3}catch{}return Date.now()+V},h=class{constructor(n,e=()=>{}){this.command=n;this.onFetch=e}command;onFetch;cached;pending;get(){return this.cached&&this.cached.expiresAt-z>Date.now()?Promise.resolve(this.cached.value):this.fetch(this.command)}async renew(n){return await this.pending?.catch(()=>{}),this.cached&&this.cached.value!==n?this.cached.value:(this.cached=void 0,this.fetch(`${this.command} --force-refresh`))}fetch(n){return this.pending??=X(n).then(e=>{if(!e)throw new Error(`Couldn't get a Databricks token: \`${n}\` printed nothing.`);return this.cached={value:e,expiresAt:Q(e)},this.onFetch(this.cached.expiresAt),e}).finally(()=>{this.pending=void 0}),this.pending}};var i=x(require("vscode")),_=3,Z=1e3,y=t=>t instanceof i.LanguageModelTextPart?t.value:t instanceof i.LanguageModelDataPart&&!t.mimeType.startsWith("image/")?new TextDecoder().decode(t.data):t instanceof i.LanguageModelPromptTsxPart?JSON.stringify(t.value):"",S=t=>t instanceof i.LanguageModelDataPart&&t.mimeType.startsWith("image/")?`data:${t.mimeType};base64,${Buffer.from(t.data).toString("base64")}`:void 0,R=t=>t.inputSchema??{type:"object",properties:{}},L=(t,n,e)=>{let s;try{s=e.trim()?JSON.parse(e):{}}catch{s=void 0}if(typeof s!="object"||s===null||Array.isArray(s))throw new Error(`The model called ${n} with arguments that aren't a JSON object: ${e}`);return new i.LanguageModelToolCallPart(t,n,s)},P=()=>new Error("The model stopped at its output token limit before finishing the response."),A=(t,n)=>t.tools.length?{tools:t.tools.map(n),...t.toolRequired?{tool_choice:"required"}:{}}:{},E=t=>{if(typeof t=="string")return Math.ceil(t.length/4);let n=0,e=0;for(let s of t.content){let a=s instanceof i.LanguageModelToolResultPart?s.content:[s];for(let o of a)n+=o instanceof i.LanguageModelToolCallPart?o.name.length+JSON.stringify(o.input).length:y(o).length,e+=S(o)===void 0?0:1}return Math.ceil(n/4)+e*Z},D=t=>({results:t.filter(n=>n instanceof i.LanguageModelToolResultPart),calls:t.filter(n=>n instanceof i.LanguageModelToolCallPart),text:t.map(y).join(""),images:t.map(S).filter(n=>n!==void 0)});function O(t){let n=t.messages.flatMap(e=>{let{results:s,calls:a,text:o,images:u}=D(e.content);if(Number(e.role)===_)return[{role:"system",content:o}];if(e.role===i.LanguageModelChatMessageRole.Assistant){let r=a.map(g=>({id:g.callId,type:"function",function:{name:g.name,arguments:JSON.stringify(g.input)}}));return o||r.length?[{role:"assistant",content:o||null,...r.length?{tool_calls:r}:{}}]:[]}let l=[...o?[{type:"text",text:o}]:[],...u.map(r=>({type:"image_url",image_url:{url:r}}))];return[...s.map(r=>({role:"tool",tool_call_id:r.callId,content:r.content.map(y).join("")})),...l.length?[{role:"user",content:l}]:[]]});return{model:t.model,messages:n,stream:!0,...A(t,e=>({type:"function",function:{name:e.name,description:e.description,parameters:R(e)}}))}}async function*I(t){let n=[];for await(let e of t)for(let s of e.choices??[]){s.delta?.content&&(yield new i.LanguageModelTextPart(s.delta.content));for(let a of s.delta?.tool_calls??[]){let o=n[a.index??0]??={id:"",name:"",args:""};o.id=a.id||o.id,o.name=a.function?.name||o.name,o.args+=a.function?.arguments??""}if(s.finish_reason==="length")throw P()}for(let e of n.filter(Boolean))yield L(e.id,e.name,e.args)}function j(t){let n=t.messages.flatMap(e=>{let{results:s,calls:a,text:o,images:u}=D(e.content);if(Number(e.role)===_)return[{role:"system",content:o}];if(e.role===i.LanguageModelChatMessageRole.Assistant)return[...o?[{role:"assistant",content:o}]:[],...a.map(r=>({type:"function_call",call_id:r.callId,name:r.name,arguments:JSON.stringify(r.input)}))];let l=[...o?[{type:"input_text",text:o}]:[],...u.map(r=>({type:"input_image",image_url:r,detail:"auto"}))];return[...s.map(r=>({type:"function_call_output",call_id:r.callId,output:r.content.map(y).join("")})),...l.length?[{role:"user",content:l}]:[]]});return{model:t.model,input:n,stream:!0,store:!1,...A(t,e=>({type:"function",name:e.name,description:e.description,parameters:R(e),strict:!1}))}}async function*G(t){for await(let n of t){let e=n;if(e.type==="response.output_text.delta"&&e.delta)yield new i.LanguageModelTextPart(e.delta);else if(e.type==="response.output_item.done"&&e.item?.type==="function_call")yield L(e.item.call_id??"",e.item.name??"",e.item.arguments??"");else{if(e.type==="response.incomplete"&&e.response?.incomplete_details?.reason==="max_output_tokens")throw P();if(e.type==="response.failed"||e.type==="error")throw new Error(e.response?.error?.message??e.message??"The model response failed.")}}}async function*k(t,n,e,s,a,o){let u=m=>fetch(`${t}/ai-gateway/mlflow/v1/${n}`,{method:"POST",headers:{...a,Authorization:`Bearer ${m}`,"Content-Type":"application/json"},body:JSON.stringify(e),signal:o}),l=await s.get(),r=await u(l);if(r.status===401&&(await r.body?.cancel(),l=await s.renew(l),r=await u(l)),r.status===401)throw await r.body?.cancel(),new Error(`Databricks rejected the sign-in. Run \`databricks auth login --host ${t}\`, then retry.`);if(!r.ok||!r.body)throw new Error(`AI Gateway returned ${r.status}: ${(await r.text()).slice(0,300)}`);let g=new TextDecoder,p="";for await(let m of r.body){p+=g.decode(m,{stream:!0});let M=p.split(` +`);p=M.pop()??"";for(let q of M){let w=$(q);if(w==="[DONE]")return;w&&(yield JSON.parse(w))}}let d=$(p+g.decode());d&&d!=="[DONE]"&&(yield JSON.parse(d))}var $=t=>t.startsWith("data:")?t.slice(5).trim():"";var N=t=>{let n=t("models");return{workspace:String(t("workspace")??"").replace(/\/+$/,""),models:(Array.isArray(n)?n:[]).filter(e=>typeof e?.id=="string"&&e.id!==""&&(e.api==="chat_completions"||e.api==="responses")),authCommand:String(t("authCommand")??"")}},v=class{constructor(n,e,s=()=>({})){this.settings=n;this.log=e;this.headers=s}settings;log;headers;changed=new c.EventEmitter;onDidChangeLanguageModelChatInformation=this.changed.event;tokens;refresh(){this.tokens=void 0,this.changed.fire()}async provideLanguageModelChatInformation(){let{workspace:n,models:e,authCommand:s}=this.settings();return!n||!s||e.length===0?(this.log("No Databricks models: run `ug configure --agents vscode` to set them up."),[]):e.map(({id:a,api:o})=>({id:a,api:o,name:a,family:a.split(".").pop()??a,version:b.version,detail:"Databricks",maxInputTokens:128e3,maxOutputTokens:32e3,capabilities:{toolCalling:!0,imageInput:o==="responses"||a.includes("claude")}}))}async provideLanguageModelChatResponse(n,e,s,a,o){let{workspace:u,authCommand:l}=this.settings();this.tokens??=new h(l,d=>this.log(`Fetched a Databricks token (expires ${new Date(d).toISOString()})`));let r={model:n.id,messages:e,tools:s.tools??[],toolRequired:s.toolMode===c.LanguageModelChatToolMode.Required},g=new AbortController,p=o.onCancellationRequested(()=>g.abort());try{let d=n.api==="responses"?G(k(u,"responses",j(r),this.tokens,this.headers(),g.signal)):I(k(u,"chat/completions",O(r),this.tokens,this.headers(),g.signal));for await(let m of d)a.report(m)}catch(d){if(!o.isCancellationRequested)throw this.log(`${n.id} failed: ${d instanceof Error?d.message:String(d)}`),d}finally{p.dispose()}}async provideTokenCount(n,e){return E(e)}};function ee(t){let n=c.window.createOutputChannel("Unity Gateway",{log:!0}),e=()=>{let o=c.workspace.getConfiguration("unityGateway");return N(u=>o.get(u))},s=()=>{let o=c.extensions.getExtension("GitHub.copilot-chat")?.packageJSON?.version;return{"User-Agent":`ucode/${b.version} githubcopilotchat${typeof o=="string"?`/${o}`:""}`}},a=new v(e,o=>n.info(o),s);t.subscriptions.push(n,c.lm.registerLanguageModelChatProvider("databricks-unity-gateway",a),c.commands.registerCommand("unityGateway.refreshModels",()=>a.refresh()),c.workspace.onDidChangeConfiguration(o=>{o.affectsConfiguration("unityGateway")&&a.refresh()}))}0&&(module.exports={DatabricksModels,activate,readSettings}); diff --git a/src/ucode/vscode/extension/package.json b/src/ucode/vscode/extension/package.json new file mode 100644 index 000000000..1b2269dd4 --- /dev/null +++ b/src/ucode/vscode/extension/package.json @@ -0,0 +1,95 @@ +{ + "name": "unity-gateway", + "displayName": "Databricks Unity Gateway", + "description": "Chat with Databricks AI Gateway models in VS Code through Unity Gateway (ug)", + "version": "0.1.0", + "publisher": "databricks", + "repository": { + "type": "git", + "url": "https://github.com/databricks/unity-gateway.git", + "directory": "vscode-extension" + }, + "engines": { + "vscode": "^1.128.0" + }, + "categories": [ + "AI", + "Chat" + ], + "extensionKind": [ + "ui" + ], + "main": "./out/extension.js", + "activationEvents": [], + "contributes": { + "languageModelChatProviders": [ + { + "vendor": "databricks-unity-gateway", + "displayName": "Databricks" + } + ], + "configuration": { + "title": "Databricks Unity Gateway", + "properties": { + "unityGateway.workspace": { + "type": "string", + "default": "", + "description": "Databricks workspace URL, for example https://my-workspace.cloud.databricks.com. Set by `ug configure`.", + "scope": "application" + }, + "unityGateway.models": { + "type": "array", + "default": [], + "description": "Models to offer in the chat model picker. Set by `ug configure`.", + "items": { + "type": "object", + "required": [ + "id", + "api" + ], + "properties": { + "id": { + "type": "string", + "description": "Model service name, for example system.ai.claude-sonnet-5-5." + }, + "api": { + "type": "string", + "enum": [ + "chat_completions", + "responses" + ], + "description": "The AI Gateway API the model is called through." + } + } + }, + "scope": "application" + }, + "unityGateway.authCommand": { + "type": "string", + "default": "", + "description": "Command that prints a Databricks access token. Set by `ug configure`.", + "scope": "application" + } + } + }, + "commands": [ + { + "command": "unityGateway.refreshModels", + "title": "Refresh Models", + "category": "Unity Gateway" + } + ] + }, + "scripts": { + "build": "esbuild src/extension.ts --bundle --minify --legal-comments=none --outfile=../src/ucode/vscode/extension/out/extension.js --external:vscode --format=cjs --platform=node --target=node22 && node -e \"require('fs').copyFileSync('package.json', '../src/ucode/vscode/extension/package.json')\"", + "typecheck": "tsc", + "test": "vitest run" + }, + "devDependencies": { + "@types/node": "^22.20.3", + "@types/vscode": "1.125.0", + "esbuild": "^0.28.2", + "typescript": "^5.9.3", + "vitest": "^4.1.12" + } +} diff --git a/vscode-extension/.gitignore b/vscode-extension/.gitignore new file mode 100644 index 000000000..28a78a7cd --- /dev/null +++ b/vscode-extension/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +*.vsix diff --git a/vscode-extension/.npmrc b/vscode-extension/.npmrc new file mode 100644 index 000000000..8f1943004 --- /dev/null +++ b/vscode-extension/.npmrc @@ -0,0 +1,3 @@ +# The lockfile records the public registry; this lets `npm ci` fetch the same tarballs from +# whichever registry is configured locally. +replace-registry-host=always diff --git a/vscode-extension/README.md b/vscode-extension/README.md new file mode 100644 index 000000000..d49c502a9 --- /dev/null +++ b/vscode-extension/README.md @@ -0,0 +1,22 @@ +# Unity Gateway for VS Code + +Adds Databricks AI Gateway models to the VS Code chat model picker, for Copilot Chat in Ask and Agent +mode. The extension calls the workspace's AI Gateway directly: Chat Completions for Claude and open +models, Responses for GPT-5 and later. + +`ug configure` installs it and writes its settings: + +| Setting | Value | +|---|---| +| `unityGateway.workspace` | The workspace URL | +| `unityGateway.models` | `[{"id": "", "api": "chat_completions" \| "responses"}]` | +| `unityGateway.authCommand` | A command that prints a Databricks access token (`ug auth-token --host ...`) | + +The token is kept in memory until it nears expiry, and fetched again when the gateway answers 401. +Run **Unity Gateway: Refresh Models** after changing the settings by hand. Token fetches and failed +requests are logged to the **Unity Gateway** output channel. + +## Development + +`npm ci`, then `npm run typecheck`, `npm test` and `npm run build`. The build writes the bundle and +`package.json` to `../src/ucode/vscode/extension/`, which ug packs from; commit them (`just vscode-build`). diff --git a/vscode-extension/package-lock.json b/vscode-extension/package-lock.json new file mode 100644 index 000000000..5e070f430 --- /dev/null +++ b/vscode-extension/package-lock.json @@ -0,0 +1,1719 @@ +{ + "name": "unity-gateway", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "unity-gateway", + "version": "0.1.0", + "devDependencies": { + "@types/node": "^22.20.3", + "@types/vscode": "1.125.0", + "esbuild": "^0.28.2", + "typescript": "^5.9.3", + "vitest": "^4.1.12" + }, + "engines": { + "vscode": "^1.128.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.154.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.154.0.tgz", + "integrity": "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.14.tgz", + "integrity": "sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.14.tgz", + "integrity": "sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.14.tgz", + "integrity": "sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.14.tgz", + "integrity": "sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.14.tgz", + "integrity": "sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.14.tgz", + "integrity": "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.14.tgz", + "integrity": "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.14.tgz", + "integrity": "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.14.tgz", + "integrity": "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.14.tgz", + "integrity": "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.14.tgz", + "integrity": "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.14.tgz", + "integrity": "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.14.tgz", + "integrity": "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.14.tgz", + "integrity": "sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.14.tgz", + "integrity": "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.125.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.125.0.tgz", + "integrity": "sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.12.tgz", + "integrity": "sha512-vFlTt+YEu9WBpNjdGayNmlMVzrWPi1+sv853/7t2FU7hpO5gAFBKHzQmkCq5Xjpr+evegF2994LQT3hF3mRlqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.12", + "@vitest/utils": "4.1.12", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.12.tgz", + "integrity": "sha512-fKShNpKZzupWZuY0z7egydvL0sqW4INEZBEExMBMAa6izlENEVHwoXgdaTCaBZ5fwlr4xf2o1wXC83n9RpO+NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.12", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.12.tgz", + "integrity": "sha512-XW9O8+Mxx4VpCtkYdMJLZQQwsVryP9wk7cPKDxtXFfwSak6yRh5+2fep/Y0XF/ElBvJHq8bcYdi6aeuai0opZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.12.tgz", + "integrity": "sha512-NKlVKCr5QnCe8jijdMnmkpHHU+WTqiLFR6G0pHji9uRjXK9xJl1k1nTw6F3pOOj2PDcsDt2GKQMI7wIsaT491w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.12", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.12.tgz", + "integrity": "sha512-ko1y1Qgb/slzQ9mr+b89Cuqud8WG1TKtsPD8r7ZUBdvVOGt85Af2HwCqHujrhwJq95hs/9RM7fcAPg57o1vlzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.12", + "@vitest/utils": "4.1.12", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.12.tgz", + "integrity": "sha512-RGq9x4JNGNTZHEJxvN0oSiJjeSCExm0n2gZKKJZHY9cX5AVA6M7VJILBT3ZJ/tJ/0J9uWT7fETV9/btoi//vSg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.12.tgz", + "integrity": "sha512-Q3awXrwOfEV+qc3WmO5ONyE0yO7Q1gQ9/oyHxGLZ/pl7N3NO/ZztqQwKLuwMkV+CA1JYSIeO3gkHnRDIo4AVaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.12", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.14.tgz", + "integrity": "sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.154.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.14", + "@rolldown/binding-android-arm64": "1.2.14", + "@rolldown/binding-darwin-arm64": "1.2.14", + "@rolldown/binding-darwin-x64": "1.2.14", + "@rolldown/binding-freebsd-x64": "1.2.14", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.14", + "@rolldown/binding-linux-arm64-gnu": "1.2.14", + "@rolldown/binding-linux-arm64-musl": "1.2.14", + "@rolldown/binding-linux-ppc64-gnu": "1.2.14", + "@rolldown/binding-linux-s390x-gnu": "1.2.14", + "@rolldown/binding-linux-x64-gnu": "1.2.14", + "@rolldown/binding-linux-x64-musl": "1.2.14", + "@rolldown/binding-openharmony-arm64": "1.2.14", + "@rolldown/binding-win32-arm64-msvc": "1.2.14", + "@rolldown/binding-win32-x64-msvc": "1.2.14" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.2.0.tgz", + "integrity": "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "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/vite": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz", + "integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.29", + "rolldown": "~1.2.12", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.12.tgz", + "integrity": "sha512-5eWdz2yb3ajxWF3C8C4TjkXl7FGm3n2E/VlazALrQHzK7nYMSGFnCZto0bbPNl77AwkOZVLk0YnBAejUu0NiWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.12", + "@vitest/mocker": "4.1.12", + "@vitest/pretty-format": "4.1.12", + "@vitest/runner": "4.1.12", + "@vitest/snapshot": "4.1.12", + "@vitest/spy": "4.1.12", + "@vitest/utils": "4.1.12", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.12", + "@vitest/browser-preview": "4.1.12", + "@vitest/browser-webdriverio": "^4.1.11", + "@vitest/coverage-istanbul": "4.1.12", + "@vitest/coverage-v8": "4.1.12", + "@vitest/ui": "4.1.12", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/vscode-extension/package.json b/vscode-extension/package.json new file mode 100644 index 000000000..1b2269dd4 --- /dev/null +++ b/vscode-extension/package.json @@ -0,0 +1,95 @@ +{ + "name": "unity-gateway", + "displayName": "Databricks Unity Gateway", + "description": "Chat with Databricks AI Gateway models in VS Code through Unity Gateway (ug)", + "version": "0.1.0", + "publisher": "databricks", + "repository": { + "type": "git", + "url": "https://github.com/databricks/unity-gateway.git", + "directory": "vscode-extension" + }, + "engines": { + "vscode": "^1.128.0" + }, + "categories": [ + "AI", + "Chat" + ], + "extensionKind": [ + "ui" + ], + "main": "./out/extension.js", + "activationEvents": [], + "contributes": { + "languageModelChatProviders": [ + { + "vendor": "databricks-unity-gateway", + "displayName": "Databricks" + } + ], + "configuration": { + "title": "Databricks Unity Gateway", + "properties": { + "unityGateway.workspace": { + "type": "string", + "default": "", + "description": "Databricks workspace URL, for example https://my-workspace.cloud.databricks.com. Set by `ug configure`.", + "scope": "application" + }, + "unityGateway.models": { + "type": "array", + "default": [], + "description": "Models to offer in the chat model picker. Set by `ug configure`.", + "items": { + "type": "object", + "required": [ + "id", + "api" + ], + "properties": { + "id": { + "type": "string", + "description": "Model service name, for example system.ai.claude-sonnet-5-5." + }, + "api": { + "type": "string", + "enum": [ + "chat_completions", + "responses" + ], + "description": "The AI Gateway API the model is called through." + } + } + }, + "scope": "application" + }, + "unityGateway.authCommand": { + "type": "string", + "default": "", + "description": "Command that prints a Databricks access token. Set by `ug configure`.", + "scope": "application" + } + } + }, + "commands": [ + { + "command": "unityGateway.refreshModels", + "title": "Refresh Models", + "category": "Unity Gateway" + } + ] + }, + "scripts": { + "build": "esbuild src/extension.ts --bundle --minify --legal-comments=none --outfile=../src/ucode/vscode/extension/out/extension.js --external:vscode --format=cjs --platform=node --target=node22 && node -e \"require('fs').copyFileSync('package.json', '../src/ucode/vscode/extension/package.json')\"", + "typecheck": "tsc", + "test": "vitest run" + }, + "devDependencies": { + "@types/node": "^22.20.3", + "@types/vscode": "1.125.0", + "esbuild": "^0.28.2", + "typescript": "^5.9.3", + "vitest": "^4.1.12" + } +} diff --git a/vscode-extension/src/auth.ts b/vscode-extension/src/auth.ts new file mode 100644 index 000000000..d5ac02555 --- /dev/null +++ b/vscode-extension/src/auth.ts @@ -0,0 +1,73 @@ +import { exec } from "node:child_process"; + +// Fetch a new token this long before the current one expires. +const EXPIRY_MARGIN_MS = 5 * 60_000; +// Tokens without a readable `exp` (a PAT, say) are fetched again after an hour. +const DEFAULT_LIFETIME_MS = 60 * 60_000; + +const run = (command: string): Promise => + new Promise((resolve, reject) => { + exec(command, { timeout: 60_000, windowsHide: true }, (error, stdout, stderr) => { + if (error) { + reject(new Error(`Couldn't get a Databricks token: \`${command}\` failed: ${String(stderr).trim().slice(0, 500) || error.message}`)); + } else { + resolve(String(stdout).trim()); + } + }); + }); + +export const expiresAt = (token: string): number => { + try { + const { exp } = JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64url").toString()); + if (typeof exp === "number") { + return exp * 1000; + } + } catch { + // Not a JWT. + } + return Date.now() + DEFAULT_LIFETIME_MS; +}; + +/** Runs the auth command for a token and keeps it until it nears expiry or is rejected. */ +export class Tokens { + private cached: { value: string; expiresAt: number } | undefined; + private pending: Promise | undefined; + + constructor( + private readonly command: string, + private readonly onFetch: (expiresAt: number) => void = () => {}, + ) {} + + get(): Promise { + if (this.cached && this.cached.expiresAt - EXPIRY_MARGIN_MS > Date.now()) { + return Promise.resolve(this.cached.value); + } + return this.fetch(this.command); + } + + /** A token the gateway rejected. The CLI can hand back its cached copy while `exp` is still ahead, so force a new one. */ + async renew(rejected: string): Promise { + await this.pending?.catch(() => {}); + if (this.cached && this.cached.value !== rejected) { + return this.cached.value; + } + this.cached = undefined; + return this.fetch(`${this.command} --force-refresh`); + } + + private fetch(command: string): Promise { + this.pending ??= run(command) + .then((value) => { + if (!value) { + throw new Error(`Couldn't get a Databricks token: \`${command}\` printed nothing.`); + } + this.cached = { value, expiresAt: expiresAt(value) }; + this.onFetch(this.cached.expiresAt); + return value; + }) + .finally(() => { + this.pending = undefined; + }); + return this.pending; + } +} diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts new file mode 100644 index 000000000..1ccd159bb --- /dev/null +++ b/vscode-extension/src/extension.ts @@ -0,0 +1,127 @@ +import * as vscode from "vscode"; +import packageJson from "../package.json"; +import { Tokens } from "./auth"; +import { chatCompletionsReplies, chatCompletionsRequest, estimateTokens, responsesReplies, responsesRequest, type Reply } from "./formats"; +import { streamEvents } from "./gateway"; + +export interface ModelSetting { + readonly id: string; + readonly api: "chat_completions" | "responses"; +} + +export interface Settings { + readonly workspace: string; + readonly models: readonly ModelSetting[]; + readonly authCommand: string; +} + +export const readSettings = (get: (key: string) => unknown): Settings => { + const models = get("models"); + return { + workspace: String(get("workspace") ?? "").replace(/\/+$/, ""), + models: (Array.isArray(models) ? models : []).filter( + (model): model is ModelSetting => + typeof model?.id === "string" && model.id !== "" && (model.api === "chat_completions" || model.api === "responses"), + ), + authCommand: String(get("authCommand") ?? ""), + }; +}; + +type Model = vscode.LanguageModelChatInformation & { readonly api: ModelSetting["api"] }; + +export class DatabricksModels implements vscode.LanguageModelChatProvider { + private readonly changed = new vscode.EventEmitter(); + readonly onDidChangeLanguageModelChatInformation = this.changed.event; + private tokens: Tokens | undefined; + + constructor( + private readonly settings: () => Settings, + private readonly log: (line: string) => void, + private readonly headers: () => Record = () => ({}), + ) {} + + /** Re-reads the settings: drops the cached token and asks VS Code to list the models again. */ + refresh(): void { + this.tokens = undefined; + this.changed.fire(); + } + + async provideLanguageModelChatInformation(): Promise { + const { workspace, models, authCommand } = this.settings(); + if (!workspace || !authCommand || models.length === 0) { + this.log("No Databricks models: run `ug configure --agents vscode` to set them up."); + return []; + } + return models.map(({ id, api }) => ({ + id, + api, + name: id, + // Copilot Chat tunes its prompts and tools by family, so this is the model's own name. + family: id.split(".").pop() ?? id, + version: packageJson.version, + detail: "Databricks", + // The gateway reports no limits for these models. + maxInputTokens: 128_000, + maxOutputTokens: 32_000, + capabilities: { toolCalling: true, imageInput: api === "responses" || id.includes("claude") }, + })); + } + + async provideLanguageModelChatResponse( + model: Model, + messages: readonly vscode.LanguageModelChatRequestMessage[], + options: vscode.ProvideLanguageModelChatResponseOptions, + progress: vscode.Progress, + token: vscode.CancellationToken, + ): Promise { + const { workspace, authCommand } = this.settings(); + this.tokens ??= new Tokens(authCommand, (expiresAt) => this.log(`Fetched a Databricks token (expires ${new Date(expiresAt).toISOString()})`)); + const turn = { model: model.id, messages, tools: options.tools ?? [], toolRequired: options.toolMode === vscode.LanguageModelChatToolMode.Required }; + const abort = new AbortController(); + const cancelled = token.onCancellationRequested(() => abort.abort()); + try { + const replies: AsyncIterable = + model.api === "responses" + ? responsesReplies(streamEvents(workspace, "responses", responsesRequest(turn), this.tokens, this.headers(), abort.signal)) + : chatCompletionsReplies(streamEvents(workspace, "chat/completions", chatCompletionsRequest(turn), this.tokens, this.headers(), abort.signal)); + for await (const reply of replies) { + progress.report(reply); + } + } catch (error) { + if (!token.isCancellationRequested) { + this.log(`${model.id} failed: ${error instanceof Error ? error.message : String(error)}`); + throw error; + } + } finally { + cancelled.dispose(); + } + } + + async provideTokenCount(_model: Model, input: string | vscode.LanguageModelChatRequestMessage): Promise { + return estimateTokens(input); + } +} + +export function activate(context: vscode.ExtensionContext): void { + const output = vscode.window.createOutputChannel("Unity Gateway", { log: true }); + const settings = () => { + const configuration = vscode.workspace.getConfiguration("unityGateway"); + return readSettings((key) => configuration.get(key)); + }; + // The gateway attributes requests by this User-Agent. + const headers = () => { + const version: unknown = vscode.extensions.getExtension("GitHub.copilot-chat")?.packageJSON?.version; + return { "User-Agent": `ucode/${packageJson.version} githubcopilotchat${typeof version === "string" ? `/${version}` : ""}` }; + }; + const provider = new DatabricksModels(settings, (line) => output.info(line), headers); + context.subscriptions.push( + output, + vscode.lm.registerLanguageModelChatProvider("databricks-unity-gateway", provider), + vscode.commands.registerCommand("unityGateway.refreshModels", () => provider.refresh()), + vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration("unityGateway")) { + provider.refresh(); + } + }), + ); +} diff --git a/vscode-extension/src/formats.ts b/vscode-extension/src/formats.ts new file mode 100644 index 000000000..7de8819f0 --- /dev/null +++ b/vscode-extension/src/formats.ts @@ -0,0 +1,194 @@ +import * as vscode from "vscode"; + +// The stable API names only User and Assistant; Copilot Chat sends its system prompt with role 3. +const SYSTEM_ROLE = 3; + +export interface Turn { + readonly model: string; + readonly messages: readonly vscode.LanguageModelChatRequestMessage[]; + readonly tools: readonly vscode.LanguageModelChatTool[]; + readonly toolRequired: boolean; +} + +export type Reply = vscode.LanguageModelTextPart | vscode.LanguageModelToolCallPart; + +const IMAGE_TOKENS = 1000; + +const textOf = (part: unknown): string => { + if (part instanceof vscode.LanguageModelTextPart) { + return part.value; + } + if (part instanceof vscode.LanguageModelDataPart && !part.mimeType.startsWith("image/")) { + return new TextDecoder().decode(part.data); + } + if (part instanceof vscode.LanguageModelPromptTsxPart) { + return JSON.stringify(part.value); + } + return ""; +}; + +const imageUrl = (part: unknown): string | undefined => + part instanceof vscode.LanguageModelDataPart && part.mimeType.startsWith("image/") + ? `data:${part.mimeType};base64,${Buffer.from(part.data).toString("base64")}` + : undefined; + +// The gateway requires a schema on every function tool; VS Code omits it for tools that take no input. +const parametersOf = (tool: vscode.LanguageModelChatTool): object => tool.inputSchema ?? { type: "object", properties: {} }; + +const toolCall = (callId: string, name: string, args: string): vscode.LanguageModelToolCallPart => { + let input: unknown; + try { + input = args.trim() ? JSON.parse(args) : {}; + } catch { + input = undefined; + } + if (typeof input !== "object" || input === null || Array.isArray(input)) { + throw new Error(`The model called ${name} with arguments that aren't a JSON object: ${args}`); + } + return new vscode.LanguageModelToolCallPart(callId, name, input); +}; + +const outputLimit = () => new Error("The model stopped at its output token limit before finishing the response."); + +const toolOptions = (turn: Turn, toTool: (tool: vscode.LanguageModelChatTool) => object): object => + turn.tools.length ? { tools: turn.tools.map(toTool), ...(turn.toolRequired ? { tool_choice: "required" } : {}) } : {}; + +/** A rough count: about four characters per token, and a flat cost per image. */ +export const estimateTokens = (input: string | vscode.LanguageModelChatRequestMessage): number => { + if (typeof input === "string") { + return Math.ceil(input.length / 4); + } + let characters = 0; + let images = 0; + for (const part of input.content) { + const items = part instanceof vscode.LanguageModelToolResultPart ? part.content : [part]; + for (const item of items) { + characters += item instanceof vscode.LanguageModelToolCallPart ? item.name.length + JSON.stringify(item.input).length : textOf(item).length; + images += imageUrl(item) === undefined ? 0 : 1; + } + } + return Math.ceil(characters / 4) + images * IMAGE_TOKENS; +}; + +/** Splits a message into its tool results, tool calls, other text, and images. */ +const sortParts = (content: readonly unknown[]) => ({ + results: content.filter((part) => part instanceof vscode.LanguageModelToolResultPart), + calls: content.filter((part) => part instanceof vscode.LanguageModelToolCallPart), + text: content.map(textOf).join(""), + images: content.map(imageUrl).filter((url) => url !== undefined), +}); + +export function chatCompletionsRequest(turn: Turn): object { + const messages = turn.messages.flatMap((message): object[] => { + const { results, calls, text, images } = sortParts(message.content); + if (Number(message.role) === SYSTEM_ROLE) { + return [{ role: "system", content: text }]; + } + if (message.role === vscode.LanguageModelChatMessageRole.Assistant) { + const toolCalls = calls.map((call) => ({ + id: call.callId, + type: "function", + function: { name: call.name, arguments: JSON.stringify(call.input) }, + })); + return text || toolCalls.length ? [{ role: "assistant", content: text || null, ...(toolCalls.length ? { tool_calls: toolCalls } : {}) }] : []; + } + const content = [ + ...(text ? [{ type: "text", text }] : []), + ...images.map((url) => ({ type: "image_url", image_url: { url } })), + ]; + return [ + ...results.map((result) => ({ role: "tool", tool_call_id: result.callId, content: result.content.map(textOf).join("") })), + ...(content.length ? [{ role: "user", content }] : []), + ]; + }); + return { + model: turn.model, + messages, + stream: true, + ...toolOptions(turn, (tool) => ({ type: "function", function: { name: tool.name, description: tool.description, parameters: parametersOf(tool) } })), + }; +} + +interface ChatChunk { + choices?: { + delta?: { content?: string | null; tool_calls?: { index?: number; id?: string; function?: { name?: string; arguments?: string } }[] }; + finish_reason?: string | null; + }[]; +} + +export async function* chatCompletionsReplies(events: AsyncIterable): AsyncGenerator { + const calls: { id: string; name: string; args: string }[] = []; + for await (const event of events) { + for (const choice of (event as ChatChunk).choices ?? []) { + if (choice.delta?.content) { + yield new vscode.LanguageModelTextPart(choice.delta.content); + } + for (const fragment of choice.delta?.tool_calls ?? []) { + const call = (calls[fragment.index ?? 0] ??= { id: "", name: "", args: "" }); + call.id = fragment.id || call.id; + call.name = fragment.function?.name || call.name; + call.args += fragment.function?.arguments ?? ""; + } + if (choice.finish_reason === "length") { + throw outputLimit(); + } + } + } + for (const call of calls.filter(Boolean)) { + yield toolCall(call.id, call.name, call.args); + } +} + +// Stateless: the gateway keeps no response state, so every turn resends the whole conversation. +export function responsesRequest(turn: Turn): object { + const input = turn.messages.flatMap((message): object[] => { + const { results, calls, text, images } = sortParts(message.content); + if (Number(message.role) === SYSTEM_ROLE) { + return [{ role: "system", content: text }]; + } + if (message.role === vscode.LanguageModelChatMessageRole.Assistant) { + return [ + ...(text ? [{ role: "assistant", content: text }] : []), + ...calls.map((call) => ({ type: "function_call", call_id: call.callId, name: call.name, arguments: JSON.stringify(call.input) })), + ]; + } + const content = [ + ...(text ? [{ type: "input_text", text }] : []), + ...images.map((url) => ({ type: "input_image", image_url: url, detail: "auto" })), + ]; + return [ + ...results.map((result) => ({ type: "function_call_output", call_id: result.callId, output: result.content.map(textOf).join("") })), + ...(content.length ? [{ role: "user", content }] : []), + ]; + }); + return { + model: turn.model, + input, + stream: true, + store: false, + ...toolOptions(turn, (tool) => ({ type: "function", name: tool.name, description: tool.description, parameters: parametersOf(tool), strict: false })), + }; +} + +interface ResponseEvent { + type: string; + delta?: string; + item?: { type?: string; call_id?: string; name?: string; arguments?: string }; + response?: { incomplete_details?: { reason?: string } | null; error?: { message?: string } | null }; + message?: string; +} + +export async function* responsesReplies(events: AsyncIterable): AsyncGenerator { + for await (const raw of events) { + const event = raw as ResponseEvent; + if (event.type === "response.output_text.delta" && event.delta) { + yield new vscode.LanguageModelTextPart(event.delta); + } else if (event.type === "response.output_item.done" && event.item?.type === "function_call") { + yield toolCall(event.item.call_id ?? "", event.item.name ?? "", event.item.arguments ?? ""); + } else if (event.type === "response.incomplete" && event.response?.incomplete_details?.reason === "max_output_tokens") { + throw outputLimit(); + } else if (event.type === "response.failed" || event.type === "error") { + throw new Error(event.response?.error?.message ?? event.message ?? "The model response failed."); + } + } +} diff --git a/vscode-extension/src/gateway.ts b/vscode-extension/src/gateway.ts new file mode 100644 index 000000000..96d5eafdd --- /dev/null +++ b/vscode-extension/src/gateway.ts @@ -0,0 +1,55 @@ +import type { Tokens } from "./auth"; + +/** POSTs a streaming request to the workspace's AI Gateway and yields each server-sent event's JSON. */ +export async function* streamEvents( + workspace: string, + path: "chat/completions" | "responses", + body: object, + tokens: Tokens, + headers: Record, + signal: AbortSignal, +): AsyncGenerator { + const post = (token: string) => + fetch(`${workspace}/ai-gateway/mlflow/v1/${path}`, { + method: "POST", + headers: { ...headers, Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, + body: JSON.stringify(body), + signal, + }); + let token = await tokens.get(); + let response = await post(token); + if (response.status === 401) { + await response.body?.cancel(); + token = await tokens.renew(token); + response = await post(token); + } + if (response.status === 401) { + await response.body?.cancel(); + throw new Error(`Databricks rejected the sign-in. Run \`databricks auth login --host ${workspace}\`, then retry.`); + } + if (!response.ok || !response.body) { + throw new Error(`AI Gateway returned ${response.status}: ${(await response.text()).slice(0, 300)}`); + } + const decoder = new TextDecoder(); + let buffer = ""; + for await (const chunk of response.body) { + buffer += decoder.decode(chunk, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + for (const line of lines) { + const data = dataOf(line); + if (data === "[DONE]") { + return; + } + if (data) { + yield JSON.parse(data); + } + } + } + const last = dataOf(buffer + decoder.decode()); + if (last && last !== "[DONE]") { + yield JSON.parse(last); + } +} + +const dataOf = (line: string): string => (line.startsWith("data:") ? line.slice(5).trim() : ""); diff --git a/vscode-extension/test/auth.test.ts b/vscode-extension/test/auth.test.ts new file mode 100644 index 000000000..7c12ef1c6 --- /dev/null +++ b/vscode-extension/test/auth.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "vitest"; +import { expiresAt, Tokens } from "../src/auth"; +import { jwt, tokenCommand } from "./support"; + +describe("Tokens", () => { + it("reuses a token until it nears expiry", async () => { + const valid = tokenCommand([jwt(3600)]); + const tokens = new Tokens(valid.command); + expect(await tokens.get()).toBe(await tokens.get()); + expect(valid.runs()).toBe(1); + + const expiring = tokenCommand([jwt(60), jwt(3600)]); + const refreshed = new Tokens(expiring.command); + expect(await refreshed.get()).not.toBe(await refreshed.get()); + expect(expiring.runs()).toBe(2); + }); + + it("runs the command once for concurrent requests", async () => { + const source = tokenCommand([jwt(3600)]); + const tokens = new Tokens(source.command); + await Promise.all([tokens.get(), tokens.get(), tokens.get()]); + expect(source.runs()).toBe(1); + }); + + it("forces a new token after a rejection, but not after a stale one", async () => { + const source = tokenCommand(["fresh"], "cached"); + const tokens = new Tokens(source.command); + expect(await tokens.get()).toBe("cached"); + expect(await tokens.renew("cached")).toBe("fresh"); + expect(await tokens.renew("cached")).toBe("fresh"); + expect(source.runs()).toBe(2); + }); + + it("waits for a fetch in flight before deciding to force one", async () => { + const source = tokenCommand(["fresh"], "cached"); + const tokens = new Tokens(source.command); + const [first, renewed] = await Promise.all([tokens.get(), tokens.renew("cached")]); + expect([first, renewed]).toEqual(["cached", "fresh"]); + expect(source.runs()).toBe(2); + }); + + it("reports the command's failure and empty output", async () => { + await expect(new Tokens(`node -e "console.error('sign-in expired'); process.exit(1)"`).get()).rejects.toThrow("sign-in expired"); + await expect(new Tokens(`node -e ""`).get()).rejects.toThrow("printed nothing"); + }); + + it("reads exp from a JWT and gives other tokens an hour", () => { + expect(expiresAt(`h.${Buffer.from('{"exp":1800000000}').toString("base64url")}.s`)).toBe(1_800_000_000_000); + expect(expiresAt("dapi123") - Date.now()).toBeGreaterThan(59 * 60_000); + }); +}); diff --git a/vscode-extension/test/extension.test.ts b/vscode-extension/test/extension.test.ts new file mode 100644 index 000000000..9cced33c0 --- /dev/null +++ b/vscode-extension/test/extension.test.ts @@ -0,0 +1,135 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import type * as vscode from "vscode"; +import packageJson from "../package.json"; +import { Tokens } from "../src/auth"; +import { DatabricksModels, readSettings, type Settings } from "../src/extension"; +import { streamEvents } from "../src/gateway"; +import { collect, message, text, tokenCommand, USER } from "./support"; +import { LanguageModelChatToolMode, LanguageModelTextPart } from "./vscode"; + +const sse = (...data: string[]) => data.map((item) => `data: ${item}\n\n`).join(""); + +/** Replaces fetch with one that answers each request with the next status and SSE body. */ +const stubFetch = (...answers: [number, string][]) => { + const requests: { url: string; authorization: string; body: Record }[] = []; + vi.stubGlobal("fetch", async (url: string, init: RequestInit) => { + const headers = new Headers(init.headers); + requests.push({ url, authorization: headers.get("authorization") ?? "", body: JSON.parse(String(init.body)) }); + const [status, body] = answers[Math.min(requests.length - 1, answers.length - 1)] ?? [500, ""]; + // Split the body mid-line so the reader has to join chunks. + const bytes = new TextEncoder().encode(body); + const middle = Math.floor(bytes.length / 2); + return new Response(new Blob([bytes.slice(0, middle), bytes.slice(middle)]).stream(), { status }); + }); + return requests; +}; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("streamEvents", () => { + it("yields each event's JSON until [DONE]", async () => { + stubFetch([200, sse('{"a":1}', '{"b":2}', "[DONE]", '{"c":3}')]); + const tokens = new Tokens(tokenCommand(["t"]).command); + expect(await collect(streamEvents("https://ws", "responses", {}, tokens, {}, new AbortController().signal))).toEqual([{ a: 1 }, { b: 2 }]); + }); + + it("keeps a last event that has no trailing newline", async () => { + stubFetch([200, `data: {"a":1}\n\ndata: {"b":2}`]); + const tokens = new Tokens(tokenCommand(["t"]).command); + expect(await collect(streamEvents("https://ws", "responses", {}, tokens, {}, new AbortController().signal))).toEqual([{ a: 1 }, { b: 2 }]); + }); + + it("retries once with a new token after a 401, then asks the user to sign in", async () => { + let requests = stubFetch([401, ""], [200, sse("[DONE]")]); + await collect(streamEvents("https://ws", "chat/completions", {}, new Tokens(tokenCommand(["fresh"], "rejected").command), {}, new AbortController().signal)); + expect(requests.map((request) => request.authorization)).toEqual(["Bearer rejected", "Bearer fresh"]); + expect(requests[0]?.url).toBe("https://ws/ai-gateway/mlflow/v1/chat/completions"); + + requests = stubFetch([401, ""]); + await expect( + collect(streamEvents("https://ws", "responses", {}, new Tokens(tokenCommand(["a", "b"]).command), {}, new AbortController().signal)), + ).rejects.toThrow("Run `databricks auth login --host https://ws`"); + expect(requests).toHaveLength(2); + }); + + it("reports other gateway errors with their body", async () => { + stubFetch([400, '{"message":"bad tools"}']); + await expect( + collect(streamEvents("https://ws", "responses", {}, new Tokens(tokenCommand(["t"]).command), {}, new AbortController().signal)), + ).rejects.toThrow('AI Gateway returned 400: {"message":"bad tools"}'); + }); +}); + +describe("settings", () => { + // Workspace settings mustn't redirect the token or choose the command that prints it. + it("are read from user settings only", () => { + const properties = Object.values(packageJson.contributes.configuration.properties); + expect(properties.map((property) => property.scope)).toEqual(["application", "application", "application"]); + }); +}); + +describe("readSettings", () => { + it("keeps well-formed models and trims the workspace URL", () => { + const values: Record = { + workspace: "https://ws/", + authCommand: "ug auth-token", + models: [{ id: "a", api: "responses" }, { id: "b", api: "messages" }, { api: "responses" }, null, { id: "c", api: "chat_completions" }], + }; + expect(readSettings((key) => values[key])).toEqual({ + workspace: "https://ws", + authCommand: "ug auth-token", + models: [ + { id: "a", api: "responses" }, + { id: "c", api: "chat_completions" }, + ], + }); + }); +}); + +describe("DatabricksModels", () => { + const settings = (command: string): Settings => ({ + workspace: "https://ws", + authCommand: command, + models: [ + { id: "system.ai.claude-sonnet-5-5", api: "chat_completions" }, + { id: "system.ai.gpt-6-luna", api: "responses" }, + ], + }); + + it("lists the configured models, or none with a hint when settings are missing", async () => { + const logs: string[] = []; + const models = await new DatabricksModels(() => settings("cmd"), (line) => logs.push(line)).provideLanguageModelChatInformation(); + expect(models.map((model) => [model.id, model.family, model.api])).toEqual([ + ["system.ai.claude-sonnet-5-5", "claude-sonnet-5-5", "chat_completions"], + ["system.ai.gpt-6-luna", "gpt-6-luna", "responses"], + ]); + expect(await new DatabricksModels(() => ({ ...settings(""), models: [] }), (line) => logs.push(line)).provideLanguageModelChatInformation()).toEqual([]); + expect(logs).toEqual(["No Databricks models: run `ug configure --agents vscode` to set them up."]); + }); + + it("sends each model's turns to its API with the User-Agent", async () => { + const requests = stubFetch( + [200, sse('{"choices":[{"delta":{"content":"hello"}}]}', "[DONE]")], + [200, sse('{"type":"response.output_text.delta","delta":"hi"}')], + ); + const provider = new DatabricksModels(() => settings(tokenCommand(["t"]).command), () => {}, () => ({ "User-Agent": "ucode/0 githubcopilotchat" })); + const [claude, gpt] = await provider.provideLanguageModelChatInformation(); + const reported: unknown[] = []; + for (const model of [claude!, gpt!]) { + await provider.provideLanguageModelChatResponse( + model, + [message(USER, text("hi"))], + { toolMode: LanguageModelChatToolMode.Auto } as unknown as vscode.ProvideLanguageModelChatResponseOptions, + { report: (part) => reported.push(part) }, + { isCancellationRequested: false, onCancellationRequested: () => ({ dispose() {} }) } as unknown as vscode.CancellationToken, + ); + } + expect(requests.map((request) => [request.url, request.body.model])).toEqual([ + ["https://ws/ai-gateway/mlflow/v1/chat/completions", "system.ai.claude-sonnet-5-5"], + ["https://ws/ai-gateway/mlflow/v1/responses", "system.ai.gpt-6-luna"], + ]); + expect(reported).toEqual([new LanguageModelTextPart("hello"), new LanguageModelTextPart("hi")]); + }); +}); diff --git a/vscode-extension/test/formats.test.ts b/vscode-extension/test/formats.test.ts new file mode 100644 index 000000000..dff44040a --- /dev/null +++ b/vscode-extension/test/formats.test.ts @@ -0,0 +1,105 @@ +import { describe, expect, it } from "vitest"; +import { chatCompletionsReplies, chatCompletionsRequest, estimateTokens, responsesReplies, responsesRequest } from "../src/formats"; +import { ASSISTANT, call, collect, events, image, message, result, SYSTEM, text, USER, weatherTool } from "./support"; +import { LanguageModelDataPart, LanguageModelTextPart, LanguageModelToolCallPart } from "./vscode"; + +const conversation = [ + message(SYSTEM, text("Be brief.")), + message(USER, text("Weather?"), image(7)), + message(ASSISTANT, text("Checking."), call("c1", "get_weather", { city: "Paris" })), + message(USER, result("c1", text("Sunny"))), +]; +const turn = { model: "m", messages: conversation, tools: [weatherTool], toolRequired: true }; + +describe("Chat Completions", () => { + it("sends the conversation, images and tools", () => { + expect(chatCompletionsRequest(turn)).toEqual({ + model: "m", + stream: true, + messages: [ + { role: "system", content: "Be brief." }, + { role: "user", content: [{ type: "text", text: "Weather?" }, { type: "image_url", image_url: { url: "data:image/png;base64,Bw==" } }] }, + { role: "assistant", content: "Checking.", tool_calls: [{ id: "c1", type: "function", function: { name: "get_weather", arguments: '{"city":"Paris"}' } }] }, + { role: "tool", tool_call_id: "c1", content: "Sunny" }, + ], + tools: [{ type: "function", function: { name: "get_weather", description: "Weather for a city", parameters: weatherTool.inputSchema } }], + tool_choice: "required", + }); + }); + + it("gives a tool without an input schema an empty one, and omits tools when there are none", () => { + const noSchema = chatCompletionsRequest({ ...turn, tools: [{ name: "list", description: "List", inputSchema: undefined }], toolRequired: false }); + expect(noSchema).toMatchObject({ tools: [{ function: { parameters: { type: "object", properties: {} } } }] }); + expect(noSchema).not.toHaveProperty("tool_choice"); + expect(chatCompletionsRequest({ ...turn, tools: [] })).not.toHaveProperty("tools"); + }); + + it("streams text and assembles a tool call from its fragments", async () => { + const replies = await collect( + chatCompletionsReplies( + events( + { choices: [{ delta: { content: "Checking." } }] }, + { choices: [{ delta: { tool_calls: [{ index: 0, id: "c1", function: { name: "get_weather", arguments: '{"ci' } }] } }] }, + { choices: [{ delta: { tool_calls: [{ index: 0, function: { arguments: 'ty":"Paris"}' } }] }, finish_reason: "tool_calls" }] }, + ), + ), + ); + expect(replies).toEqual([new LanguageModelTextPart("Checking."), new LanguageModelToolCallPart("c1", "get_weather", { city: "Paris" })]); + }); + + it("fails on a length stop and on arguments that aren't an object", async () => { + await expect(collect(chatCompletionsReplies(events({ choices: [{ finish_reason: "length" }] })))).rejects.toThrow("output token limit"); + await expect( + collect(chatCompletionsReplies(events({ choices: [{ delta: { tool_calls: [{ index: 0, id: "c", function: { name: "t", arguments: "[1]" } }] } }] }))), + ).rejects.toThrow("aren't a JSON object"); + }); +}); + +describe("estimateTokens", () => { + it("counts text and gives each image a flat cost instead of its bytes", () => { + const screenshot = new Uint8Array(400_000); + const withImage = message(USER, text("abcd".repeat(10)), result("c1", text("ok"), image(1)), new LanguageModelDataPart(screenshot, "image/png")); + expect(estimateTokens("abcdefgh")).toBe(2); + expect(estimateTokens(withImage)).toBe(11 + 2000); + }); +}); + +describe("Responses", () => { + it("sends the whole conversation statelessly", () => { + expect(responsesRequest(turn)).toEqual({ + model: "m", + stream: true, + store: false, + input: [ + { role: "system", content: "Be brief." }, + { role: "user", content: [{ type: "input_text", text: "Weather?" }, { type: "input_image", image_url: "data:image/png;base64,Bw==", detail: "auto" }] }, + { role: "assistant", content: "Checking." }, + { type: "function_call", call_id: "c1", name: "get_weather", arguments: '{"city":"Paris"}' }, + { type: "function_call_output", call_id: "c1", output: "Sunny" }, + ], + tools: [{ type: "function", name: "get_weather", description: "Weather for a city", parameters: weatherTool.inputSchema, strict: false }], + tool_choice: "required", + }); + }); + + it("streams text and each finished function call", async () => { + const replies = await collect( + responsesReplies( + events( + { type: "response.output_text.delta", delta: "Checking." }, + { type: "response.function_call_arguments.delta", delta: '{"city"' }, + { type: "response.output_item.done", item: { type: "function_call", call_id: "c1", name: "get_weather", arguments: '{"city":"Paris"}' } }, + { type: "response.completed", response: {} }, + ), + ), + ); + expect(replies).toEqual([new LanguageModelTextPart("Checking."), new LanguageModelToolCallPart("c1", "get_weather", { city: "Paris" })]); + }); + + it("fails on a token-limit stop and on a failed response", async () => { + await expect( + collect(responsesReplies(events({ type: "response.incomplete", response: { incomplete_details: { reason: "max_output_tokens" } } }))), + ).rejects.toThrow("output token limit"); + await expect(collect(responsesReplies(events({ type: "response.failed", response: { error: { message: "overloaded" } } })))).rejects.toThrow("overloaded"); + }); +}); diff --git a/vscode-extension/test/support.ts b/vscode-extension/test/support.ts new file mode 100644 index 000000000..501abb01a --- /dev/null +++ b/vscode-extension/test/support.ts @@ -0,0 +1,54 @@ +import { mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type * as vscode from "vscode"; +import { LanguageModelDataPart, LanguageModelTextPart, LanguageModelToolCallPart, LanguageModelToolResultPart } from "./vscode"; + +export const SYSTEM = 3; +export const USER = 1; +export const ASSISTANT = 2; + +export const message = (role: number, ...content: unknown[]) => ({ role, content, name: undefined }) as unknown as vscode.LanguageModelChatRequestMessage; +export const text = (value: string) => new LanguageModelTextPart(value); +export const image = (byte: number) => new LanguageModelDataPart(new Uint8Array([byte]), "image/png"); +export const call = (id: string, name: string, input: object) => new LanguageModelToolCallPart(id, name, input); +export const result = (id: string, ...content: unknown[]) => new LanguageModelToolResultPart(id, content); +export const weatherTool: vscode.LanguageModelChatTool = { + name: "get_weather", + description: "Weather for a city", + inputSchema: { type: "object", properties: { city: { type: "string" } } }, +}; + +export const jwt = (expiresInSeconds: number) => + `h.${Buffer.from(JSON.stringify({ exp: Math.floor(Date.now() / 1000) + expiresInSeconds })).toString("base64url")}.s`; + +/** + * A real shell command that prints the next of `tokens` on each run and counts its runs. With `cached`, it + * prints `cached` until it's run with --force-refresh, like the Databricks CLI's token cache. + */ +export const tokenCommand = (tokens: string[], cached?: string) => { + const dir = mkdtempSync(join(tmpdir(), "ug-vscode-test-")); + const state = join(dir, "runs"); + writeFileSync(state, "0"); + const script = join(dir, "token.js"); + writeFileSync( + script, + `const fs = require("fs"); const n = Number(fs.readFileSync(${JSON.stringify(state)}, "utf8")); fs.writeFileSync(${JSON.stringify(state)}, String(n + 1)); + const tokens = ${JSON.stringify(tokens)}; const cached = ${JSON.stringify(cached ?? null)}; + const token = cached !== null && !process.argv.includes("--force-refresh") ? cached : tokens[Math.min(n, tokens.length - 1)]; + process.stdout.write(token + "\\n");`, + ); + return { command: `node ${JSON.stringify(script)}`, runs: () => Number(readFileSync(state, "utf8")) }; +}; + +export async function* events(...items: unknown[]): AsyncGenerator { + yield* items; +} + +export const collect = async (items: AsyncIterable): Promise => { + const out: T[] = []; + for await (const item of items) { + out.push(item); + } + return out; +}; diff --git a/vscode-extension/test/vscode.ts b/vscode-extension/test/vscode.ts new file mode 100644 index 000000000..ffb58110b --- /dev/null +++ b/vscode-extension/test/vscode.ts @@ -0,0 +1,40 @@ +// Unit tests run outside VS Code; this provides the parts of the `vscode` API the extension touches. +export class LanguageModelTextPart { + constructor(public value: string) {} +} +export class LanguageModelToolCallPart { + constructor( + public callId: string, + public name: string, + public input: object, + ) {} +} +export class LanguageModelToolResultPart { + constructor( + public callId: string, + public content: unknown[], + ) {} +} +export class LanguageModelDataPart { + constructor( + public data: Uint8Array, + public mimeType: string, + ) {} +} +export class LanguageModelPromptTsxPart { + constructor(public value: unknown) {} +} +export const LanguageModelChatMessageRole = { User: 1, Assistant: 2 } as const; +export const LanguageModelChatToolMode = { Auto: 1, Required: 2 } as const; +export class EventEmitter { + private listeners: ((value: T) => void)[] = []; + event = (listener: (value: T) => void) => { + this.listeners.push(listener); + return { dispose: () => {} }; + }; + fire(value: T): void { + for (const listener of this.listeners) { + listener(value); + } + } +} diff --git a/vscode-extension/tsconfig.json b/vscode-extension/tsconfig.json new file mode 100644 index 000000000..a12269ab6 --- /dev/null +++ b/vscode-extension/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023", "DOM", "DOM.AsyncIterable"], + "module": "Preserve", + "moduleResolution": "Bundler", + "types": ["node"], + "strict": true, + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true, + "isolatedModules": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/vscode-extension/vitest.config.ts b/vscode-extension/vitest.config.ts new file mode 100644 index 000000000..bdb97c590 --- /dev/null +++ b/vscode-extension/vitest.config.ts @@ -0,0 +1,8 @@ +import { resolve } from "node:path"; +import { defineConfig } from "vitest/config"; + +// Unit tests run outside VS Code, so `vscode` resolves to a small stand-in for the API types we use. +export default defineConfig({ + resolve: { alias: { vscode: resolve(__dirname, "test/vscode.ts") } }, + test: { include: ["test/**/*.test.ts"] }, +}); From 9d8737d8241be6db776c93f7b92e3fbca2ebd422 Mon Sep 17 00:00:00 2001 From: Xi Lyu Date: Tue, 27 Oct 2026 16:39:22 +0000 Subject: [PATCH 2/6] Run the VS Code extension's tests with Node's test runner Drops vitest, which made up about two-thirds of the lockfile. esbuild, already used for the bundle, compiles the tests with `vscode` aliased to the test stand-in, and `node --test` runs them. Nothing shipped changes. Co-authored-by: Isaac --- src/ucode/vscode/extension/out/extension.js | 4 +- src/ucode/vscode/extension/package.json | 5 +- vscode-extension/.gitignore | 1 + vscode-extension/package-lock.json | 1262 +------------------ vscode-extension/package.json | 5 +- vscode-extension/test/auth.test.ts | 36 +- vscode-extension/test/extension.test.ts | 64 +- vscode-extension/test/formats.test.ts | 39 +- vscode-extension/test/support.ts | 8 + vscode-extension/vitest.config.ts | 8 - 10 files changed, 135 insertions(+), 1297 deletions(-) delete mode 100644 vscode-extension/vitest.config.ts diff --git a/src/ucode/vscode/extension/out/extension.js b/src/ucode/vscode/extension/out/extension.js index 91bf51fda..4dc8384f1 100644 --- a/src/ucode/vscode/extension/out/extension.js +++ b/src/ucode/vscode/extension/out/extension.js @@ -1,2 +1,2 @@ -"use strict";var J=Object.create;var f=Object.defineProperty;var U=Object.getOwnPropertyDescriptor;var F=Object.getOwnPropertyNames;var B=Object.getPrototypeOf,W=Object.prototype.hasOwnProperty;var H=(t,n)=>{for(var e in n)f(t,e,{get:n[e],enumerable:!0})},C=(t,n,e,s)=>{if(n&&typeof n=="object"||typeof n=="function")for(let a of F(n))!W.call(t,a)&&a!==e&&f(t,a,{get:()=>n[a],enumerable:!(s=U(n,a))||s.enumerable});return t};var x=(t,n,e)=>(e=t!=null?J(B(t)):{},C(n||!t||!t.__esModule?f(e,"default",{value:t,enumerable:!0}):e,t)),K=t=>C(f({},"__esModule",{value:!0}),t);var te={};H(te,{DatabricksModels:()=>v,activate:()=>ee,readSettings:()=>N});module.exports=K(te);var c=x(require("vscode"));var b={name:"unity-gateway",displayName:"Databricks Unity Gateway",description:"Chat with Databricks AI Gateway models in VS Code through Unity Gateway (ug)",version:"0.1.0",publisher:"databricks",repository:{type:"git",url:"https://github.com/databricks/unity-gateway.git",directory:"vscode-extension"},engines:{vscode:"^1.128.0"},categories:["AI","Chat"],extensionKind:["ui"],main:"./out/extension.js",activationEvents:[],contributes:{languageModelChatProviders:[{vendor:"databricks-unity-gateway",displayName:"Databricks"}],configuration:{title:"Databricks Unity Gateway",properties:{"unityGateway.workspace":{type:"string",default:"",description:"Databricks workspace URL, for example https://my-workspace.cloud.databricks.com. Set by `ug configure`.",scope:"application"},"unityGateway.models":{type:"array",default:[],description:"Models to offer in the chat model picker. Set by `ug configure`.",items:{type:"object",required:["id","api"],properties:{id:{type:"string",description:"Model service name, for example system.ai.claude-sonnet-5-5."},api:{type:"string",enum:["chat_completions","responses"],description:"The AI Gateway API the model is called through."}}},scope:"application"},"unityGateway.authCommand":{type:"string",default:"",description:"Command that prints a Databricks access token. Set by `ug configure`.",scope:"application"}}},commands:[{command:"unityGateway.refreshModels",title:"Refresh Models",category:"Unity Gateway"}]},scripts:{build:`esbuild src/extension.ts --bundle --minify --legal-comments=none --outfile=../src/ucode/vscode/extension/out/extension.js --external:vscode --format=cjs --platform=node --target=node22 && node -e "require('fs').copyFileSync('package.json', '../src/ucode/vscode/extension/package.json')"`,typecheck:"tsc",test:"vitest run"},devDependencies:{"@types/node":"^22.20.3","@types/vscode":"1.125.0",esbuild:"^0.28.2",typescript:"^5.9.3",vitest:"^4.1.12"}};var T=require("node:child_process"),z=5*6e4,V=60*6e4,X=t=>new Promise((n,e)=>{(0,T.exec)(t,{timeout:6e4,windowsHide:!0},(s,a,o)=>{s?e(new Error(`Couldn't get a Databricks token: \`${t}\` failed: ${String(o).trim().slice(0,500)||s.message}`)):n(String(a).trim())})}),Q=t=>{try{let{exp:n}=JSON.parse(Buffer.from(t.split(".")[1]??"","base64url").toString());if(typeof n=="number")return n*1e3}catch{}return Date.now()+V},h=class{constructor(n,e=()=>{}){this.command=n;this.onFetch=e}command;onFetch;cached;pending;get(){return this.cached&&this.cached.expiresAt-z>Date.now()?Promise.resolve(this.cached.value):this.fetch(this.command)}async renew(n){return await this.pending?.catch(()=>{}),this.cached&&this.cached.value!==n?this.cached.value:(this.cached=void 0,this.fetch(`${this.command} --force-refresh`))}fetch(n){return this.pending??=X(n).then(e=>{if(!e)throw new Error(`Couldn't get a Databricks token: \`${n}\` printed nothing.`);return this.cached={value:e,expiresAt:Q(e)},this.onFetch(this.cached.expiresAt),e}).finally(()=>{this.pending=void 0}),this.pending}};var i=x(require("vscode")),_=3,Z=1e3,y=t=>t instanceof i.LanguageModelTextPart?t.value:t instanceof i.LanguageModelDataPart&&!t.mimeType.startsWith("image/")?new TextDecoder().decode(t.data):t instanceof i.LanguageModelPromptTsxPart?JSON.stringify(t.value):"",S=t=>t instanceof i.LanguageModelDataPart&&t.mimeType.startsWith("image/")?`data:${t.mimeType};base64,${Buffer.from(t.data).toString("base64")}`:void 0,R=t=>t.inputSchema??{type:"object",properties:{}},L=(t,n,e)=>{let s;try{s=e.trim()?JSON.parse(e):{}}catch{s=void 0}if(typeof s!="object"||s===null||Array.isArray(s))throw new Error(`The model called ${n} with arguments that aren't a JSON object: ${e}`);return new i.LanguageModelToolCallPart(t,n,s)},P=()=>new Error("The model stopped at its output token limit before finishing the response."),A=(t,n)=>t.tools.length?{tools:t.tools.map(n),...t.toolRequired?{tool_choice:"required"}:{}}:{},E=t=>{if(typeof t=="string")return Math.ceil(t.length/4);let n=0,e=0;for(let s of t.content){let a=s instanceof i.LanguageModelToolResultPart?s.content:[s];for(let o of a)n+=o instanceof i.LanguageModelToolCallPart?o.name.length+JSON.stringify(o.input).length:y(o).length,e+=S(o)===void 0?0:1}return Math.ceil(n/4)+e*Z},D=t=>({results:t.filter(n=>n instanceof i.LanguageModelToolResultPart),calls:t.filter(n=>n instanceof i.LanguageModelToolCallPart),text:t.map(y).join(""),images:t.map(S).filter(n=>n!==void 0)});function O(t){let n=t.messages.flatMap(e=>{let{results:s,calls:a,text:o,images:u}=D(e.content);if(Number(e.role)===_)return[{role:"system",content:o}];if(e.role===i.LanguageModelChatMessageRole.Assistant){let r=a.map(g=>({id:g.callId,type:"function",function:{name:g.name,arguments:JSON.stringify(g.input)}}));return o||r.length?[{role:"assistant",content:o||null,...r.length?{tool_calls:r}:{}}]:[]}let l=[...o?[{type:"text",text:o}]:[],...u.map(r=>({type:"image_url",image_url:{url:r}}))];return[...s.map(r=>({role:"tool",tool_call_id:r.callId,content:r.content.map(y).join("")})),...l.length?[{role:"user",content:l}]:[]]});return{model:t.model,messages:n,stream:!0,...A(t,e=>({type:"function",function:{name:e.name,description:e.description,parameters:R(e)}}))}}async function*I(t){let n=[];for await(let e of t)for(let s of e.choices??[]){s.delta?.content&&(yield new i.LanguageModelTextPart(s.delta.content));for(let a of s.delta?.tool_calls??[]){let o=n[a.index??0]??={id:"",name:"",args:""};o.id=a.id||o.id,o.name=a.function?.name||o.name,o.args+=a.function?.arguments??""}if(s.finish_reason==="length")throw P()}for(let e of n.filter(Boolean))yield L(e.id,e.name,e.args)}function j(t){let n=t.messages.flatMap(e=>{let{results:s,calls:a,text:o,images:u}=D(e.content);if(Number(e.role)===_)return[{role:"system",content:o}];if(e.role===i.LanguageModelChatMessageRole.Assistant)return[...o?[{role:"assistant",content:o}]:[],...a.map(r=>({type:"function_call",call_id:r.callId,name:r.name,arguments:JSON.stringify(r.input)}))];let l=[...o?[{type:"input_text",text:o}]:[],...u.map(r=>({type:"input_image",image_url:r,detail:"auto"}))];return[...s.map(r=>({type:"function_call_output",call_id:r.callId,output:r.content.map(y).join("")})),...l.length?[{role:"user",content:l}]:[]]});return{model:t.model,input:n,stream:!0,store:!1,...A(t,e=>({type:"function",name:e.name,description:e.description,parameters:R(e),strict:!1}))}}async function*G(t){for await(let n of t){let e=n;if(e.type==="response.output_text.delta"&&e.delta)yield new i.LanguageModelTextPart(e.delta);else if(e.type==="response.output_item.done"&&e.item?.type==="function_call")yield L(e.item.call_id??"",e.item.name??"",e.item.arguments??"");else{if(e.type==="response.incomplete"&&e.response?.incomplete_details?.reason==="max_output_tokens")throw P();if(e.type==="response.failed"||e.type==="error")throw new Error(e.response?.error?.message??e.message??"The model response failed.")}}}async function*k(t,n,e,s,a,o){let u=m=>fetch(`${t}/ai-gateway/mlflow/v1/${n}`,{method:"POST",headers:{...a,Authorization:`Bearer ${m}`,"Content-Type":"application/json"},body:JSON.stringify(e),signal:o}),l=await s.get(),r=await u(l);if(r.status===401&&(await r.body?.cancel(),l=await s.renew(l),r=await u(l)),r.status===401)throw await r.body?.cancel(),new Error(`Databricks rejected the sign-in. Run \`databricks auth login --host ${t}\`, then retry.`);if(!r.ok||!r.body)throw new Error(`AI Gateway returned ${r.status}: ${(await r.text()).slice(0,300)}`);let g=new TextDecoder,p="";for await(let m of r.body){p+=g.decode(m,{stream:!0});let M=p.split(` -`);p=M.pop()??"";for(let q of M){let w=$(q);if(w==="[DONE]")return;w&&(yield JSON.parse(w))}}let d=$(p+g.decode());d&&d!=="[DONE]"&&(yield JSON.parse(d))}var $=t=>t.startsWith("data:")?t.slice(5).trim():"";var N=t=>{let n=t("models");return{workspace:String(t("workspace")??"").replace(/\/+$/,""),models:(Array.isArray(n)?n:[]).filter(e=>typeof e?.id=="string"&&e.id!==""&&(e.api==="chat_completions"||e.api==="responses")),authCommand:String(t("authCommand")??"")}},v=class{constructor(n,e,s=()=>({})){this.settings=n;this.log=e;this.headers=s}settings;log;headers;changed=new c.EventEmitter;onDidChangeLanguageModelChatInformation=this.changed.event;tokens;refresh(){this.tokens=void 0,this.changed.fire()}async provideLanguageModelChatInformation(){let{workspace:n,models:e,authCommand:s}=this.settings();return!n||!s||e.length===0?(this.log("No Databricks models: run `ug configure --agents vscode` to set them up."),[]):e.map(({id:a,api:o})=>({id:a,api:o,name:a,family:a.split(".").pop()??a,version:b.version,detail:"Databricks",maxInputTokens:128e3,maxOutputTokens:32e3,capabilities:{toolCalling:!0,imageInput:o==="responses"||a.includes("claude")}}))}async provideLanguageModelChatResponse(n,e,s,a,o){let{workspace:u,authCommand:l}=this.settings();this.tokens??=new h(l,d=>this.log(`Fetched a Databricks token (expires ${new Date(d).toISOString()})`));let r={model:n.id,messages:e,tools:s.tools??[],toolRequired:s.toolMode===c.LanguageModelChatToolMode.Required},g=new AbortController,p=o.onCancellationRequested(()=>g.abort());try{let d=n.api==="responses"?G(k(u,"responses",j(r),this.tokens,this.headers(),g.signal)):I(k(u,"chat/completions",O(r),this.tokens,this.headers(),g.signal));for await(let m of d)a.report(m)}catch(d){if(!o.isCancellationRequested)throw this.log(`${n.id} failed: ${d instanceof Error?d.message:String(d)}`),d}finally{p.dispose()}}async provideTokenCount(n,e){return E(e)}};function ee(t){let n=c.window.createOutputChannel("Unity Gateway",{log:!0}),e=()=>{let o=c.workspace.getConfiguration("unityGateway");return N(u=>o.get(u))},s=()=>{let o=c.extensions.getExtension("GitHub.copilot-chat")?.packageJSON?.version;return{"User-Agent":`ucode/${b.version} githubcopilotchat${typeof o=="string"?`/${o}`:""}`}},a=new v(e,o=>n.info(o),s);t.subscriptions.push(n,c.lm.registerLanguageModelChatProvider("databricks-unity-gateway",a),c.commands.registerCommand("unityGateway.refreshModels",()=>a.refresh()),c.workspace.onDidChangeConfiguration(o=>{o.affectsConfiguration("unityGateway")&&a.refresh()}))}0&&(module.exports={DatabricksModels,activate,readSettings}); +"use strict";var J=Object.create;var f=Object.defineProperty;var U=Object.getOwnPropertyDescriptor;var F=Object.getOwnPropertyNames;var B=Object.getPrototypeOf,W=Object.prototype.hasOwnProperty;var H=(t,n)=>{for(var e in n)f(t,e,{get:n[e],enumerable:!0})},C=(t,n,e,s)=>{if(n&&typeof n=="object"||typeof n=="function")for(let a of F(n))!W.call(t,a)&&a!==e&&f(t,a,{get:()=>n[a],enumerable:!(s=U(n,a))||s.enumerable});return t};var x=(t,n,e)=>(e=t!=null?J(B(t)):{},C(n||!t||!t.__esModule?f(e,"default",{value:t,enumerable:!0}):e,t)),K=t=>C(f({},"__esModule",{value:!0}),t);var te={};H(te,{DatabricksModels:()=>v,activate:()=>ee,readSettings:()=>N});module.exports=K(te);var c=x(require("vscode"));var b={name:"unity-gateway",displayName:"Databricks Unity Gateway",description:"Chat with Databricks AI Gateway models in VS Code through Unity Gateway (ug)",version:"0.1.0",publisher:"databricks",repository:{type:"git",url:"https://github.com/databricks/unity-gateway.git",directory:"vscode-extension"},engines:{vscode:"^1.128.0"},categories:["AI","Chat"],extensionKind:["ui"],main:"./out/extension.js",activationEvents:[],contributes:{languageModelChatProviders:[{vendor:"databricks-unity-gateway",displayName:"Databricks"}],configuration:{title:"Databricks Unity Gateway",properties:{"unityGateway.workspace":{type:"string",default:"",description:"Databricks workspace URL, for example https://my-workspace.cloud.databricks.com. Set by `ug configure`.",scope:"application"},"unityGateway.models":{type:"array",default:[],description:"Models to offer in the chat model picker. Set by `ug configure`.",items:{type:"object",required:["id","api"],properties:{id:{type:"string",description:"Model service name, for example system.ai.claude-sonnet-5-5."},api:{type:"string",enum:["chat_completions","responses"],description:"The AI Gateway API the model is called through."}}},scope:"application"},"unityGateway.authCommand":{type:"string",default:"",description:"Command that prints a Databricks access token. Set by `ug configure`.",scope:"application"}}},commands:[{command:"unityGateway.refreshModels",title:"Refresh Models",category:"Unity Gateway"}]},scripts:{build:`esbuild src/extension.ts --bundle --minify --legal-comments=none --outfile=../src/ucode/vscode/extension/out/extension.js --external:vscode --format=cjs --platform=node --target=node22 && node -e "require('fs').copyFileSync('package.json', '../src/ucode/vscode/extension/package.json')"`,typecheck:"tsc",test:"esbuild test/*.test.ts --bundle --platform=node --format=cjs --target=node22 --outdir=.test-out --alias:vscode=./test/vscode.ts --log-level=warning && node --test .test-out/*.test.js"},devDependencies:{"@types/node":"^22.20.3","@types/vscode":"1.125.0",esbuild:"^0.28.2",typescript:"^5.9.3"}};var T=require("node:child_process"),z=5*6e4,V=60*6e4,X=t=>new Promise((n,e)=>{(0,T.exec)(t,{timeout:6e4,windowsHide:!0},(s,a,o)=>{s?e(new Error(`Couldn't get a Databricks token: \`${t}\` failed: ${String(o).trim().slice(0,500)||s.message}`)):n(String(a).trim())})}),Q=t=>{try{let{exp:n}=JSON.parse(Buffer.from(t.split(".")[1]??"","base64url").toString());if(typeof n=="number")return n*1e3}catch{}return Date.now()+V},h=class{constructor(n,e=()=>{}){this.command=n;this.onFetch=e}command;onFetch;cached;pending;get(){return this.cached&&this.cached.expiresAt-z>Date.now()?Promise.resolve(this.cached.value):this.fetch(this.command)}async renew(n){return await this.pending?.catch(()=>{}),this.cached&&this.cached.value!==n?this.cached.value:(this.cached=void 0,this.fetch(`${this.command} --force-refresh`))}fetch(n){return this.pending??=X(n).then(e=>{if(!e)throw new Error(`Couldn't get a Databricks token: \`${n}\` printed nothing.`);return this.cached={value:e,expiresAt:Q(e)},this.onFetch(this.cached.expiresAt),e}).finally(()=>{this.pending=void 0}),this.pending}};var i=x(require("vscode")),_=3,Z=1e3,y=t=>t instanceof i.LanguageModelTextPart?t.value:t instanceof i.LanguageModelDataPart&&!t.mimeType.startsWith("image/")?new TextDecoder().decode(t.data):t instanceof i.LanguageModelPromptTsxPart?JSON.stringify(t.value):"",S=t=>t instanceof i.LanguageModelDataPart&&t.mimeType.startsWith("image/")?`data:${t.mimeType};base64,${Buffer.from(t.data).toString("base64")}`:void 0,R=t=>t.inputSchema??{type:"object",properties:{}},L=(t,n,e)=>{let s;try{s=e.trim()?JSON.parse(e):{}}catch{s=void 0}if(typeof s!="object"||s===null||Array.isArray(s))throw new Error(`The model called ${n} with arguments that aren't a JSON object: ${e}`);return new i.LanguageModelToolCallPart(t,n,s)},P=()=>new Error("The model stopped at its output token limit before finishing the response."),A=(t,n)=>t.tools.length?{tools:t.tools.map(n),...t.toolRequired?{tool_choice:"required"}:{}}:{},E=t=>{if(typeof t=="string")return Math.ceil(t.length/4);let n=0,e=0;for(let s of t.content){let a=s instanceof i.LanguageModelToolResultPart?s.content:[s];for(let o of a)n+=o instanceof i.LanguageModelToolCallPart?o.name.length+JSON.stringify(o.input).length:y(o).length,e+=S(o)===void 0?0:1}return Math.ceil(n/4)+e*Z},j=t=>({results:t.filter(n=>n instanceof i.LanguageModelToolResultPart),calls:t.filter(n=>n instanceof i.LanguageModelToolCallPart),text:t.map(y).join(""),images:t.map(S).filter(n=>n!==void 0)});function D(t){let n=t.messages.flatMap(e=>{let{results:s,calls:a,text:o,images:u}=j(e.content);if(Number(e.role)===_)return[{role:"system",content:o}];if(e.role===i.LanguageModelChatMessageRole.Assistant){let r=a.map(g=>({id:g.callId,type:"function",function:{name:g.name,arguments:JSON.stringify(g.input)}}));return o||r.length?[{role:"assistant",content:o||null,...r.length?{tool_calls:r}:{}}]:[]}let l=[...o?[{type:"text",text:o}]:[],...u.map(r=>({type:"image_url",image_url:{url:r}}))];return[...s.map(r=>({role:"tool",tool_call_id:r.callId,content:r.content.map(y).join("")})),...l.length?[{role:"user",content:l}]:[]]});return{model:t.model,messages:n,stream:!0,...A(t,e=>({type:"function",function:{name:e.name,description:e.description,parameters:R(e)}}))}}async function*O(t){let n=[];for await(let e of t)for(let s of e.choices??[]){s.delta?.content&&(yield new i.LanguageModelTextPart(s.delta.content));for(let a of s.delta?.tool_calls??[]){let o=n[a.index??0]??={id:"",name:"",args:""};o.id=a.id||o.id,o.name=a.function?.name||o.name,o.args+=a.function?.arguments??""}if(s.finish_reason==="length")throw P()}for(let e of n.filter(Boolean))yield L(e.id,e.name,e.args)}function I(t){let n=t.messages.flatMap(e=>{let{results:s,calls:a,text:o,images:u}=j(e.content);if(Number(e.role)===_)return[{role:"system",content:o}];if(e.role===i.LanguageModelChatMessageRole.Assistant)return[...o?[{role:"assistant",content:o}]:[],...a.map(r=>({type:"function_call",call_id:r.callId,name:r.name,arguments:JSON.stringify(r.input)}))];let l=[...o?[{type:"input_text",text:o}]:[],...u.map(r=>({type:"input_image",image_url:r,detail:"auto"}))];return[...s.map(r=>({type:"function_call_output",call_id:r.callId,output:r.content.map(y).join("")})),...l.length?[{role:"user",content:l}]:[]]});return{model:t.model,input:n,stream:!0,store:!1,...A(t,e=>({type:"function",name:e.name,description:e.description,parameters:R(e),strict:!1}))}}async function*G(t){for await(let n of t){let e=n;if(e.type==="response.output_text.delta"&&e.delta)yield new i.LanguageModelTextPart(e.delta);else if(e.type==="response.output_item.done"&&e.item?.type==="function_call")yield L(e.item.call_id??"",e.item.name??"",e.item.arguments??"");else{if(e.type==="response.incomplete"&&e.response?.incomplete_details?.reason==="max_output_tokens")throw P();if(e.type==="response.failed"||e.type==="error")throw new Error(e.response?.error?.message??e.message??"The model response failed.")}}}async function*k(t,n,e,s,a,o){let u=m=>fetch(`${t}/ai-gateway/mlflow/v1/${n}`,{method:"POST",headers:{...a,Authorization:`Bearer ${m}`,"Content-Type":"application/json"},body:JSON.stringify(e),signal:o}),l=await s.get(),r=await u(l);if(r.status===401&&(await r.body?.cancel(),l=await s.renew(l),r=await u(l)),r.status===401)throw await r.body?.cancel(),new Error(`Databricks rejected the sign-in. Run \`databricks auth login --host ${t}\`, then retry.`);if(!r.ok||!r.body)throw new Error(`AI Gateway returned ${r.status}: ${(await r.text()).slice(0,300)}`);let g=new TextDecoder,p="";for await(let m of r.body){p+=g.decode(m,{stream:!0});let M=p.split(` +`);p=M.pop()??"";for(let q of M){let w=$(q);if(w==="[DONE]")return;w&&(yield JSON.parse(w))}}let d=$(p+g.decode());d&&d!=="[DONE]"&&(yield JSON.parse(d))}var $=t=>t.startsWith("data:")?t.slice(5).trim():"";var N=t=>{let n=t("models");return{workspace:String(t("workspace")??"").replace(/\/+$/,""),models:(Array.isArray(n)?n:[]).filter(e=>typeof e?.id=="string"&&e.id!==""&&(e.api==="chat_completions"||e.api==="responses")),authCommand:String(t("authCommand")??"")}},v=class{constructor(n,e,s=()=>({})){this.settings=n;this.log=e;this.headers=s}settings;log;headers;changed=new c.EventEmitter;onDidChangeLanguageModelChatInformation=this.changed.event;tokens;refresh(){this.tokens=void 0,this.changed.fire()}async provideLanguageModelChatInformation(){let{workspace:n,models:e,authCommand:s}=this.settings();return!n||!s||e.length===0?(this.log("No Databricks models: run `ug configure --agents vscode` to set them up."),[]):e.map(({id:a,api:o})=>({id:a,api:o,name:a,family:a.split(".").pop()??a,version:b.version,detail:"Databricks",maxInputTokens:128e3,maxOutputTokens:32e3,capabilities:{toolCalling:!0,imageInput:o==="responses"||a.includes("claude")}}))}async provideLanguageModelChatResponse(n,e,s,a,o){let{workspace:u,authCommand:l}=this.settings();this.tokens??=new h(l,d=>this.log(`Fetched a Databricks token (expires ${new Date(d).toISOString()})`));let r={model:n.id,messages:e,tools:s.tools??[],toolRequired:s.toolMode===c.LanguageModelChatToolMode.Required},g=new AbortController,p=o.onCancellationRequested(()=>g.abort());try{let d=n.api==="responses"?G(k(u,"responses",I(r),this.tokens,this.headers(),g.signal)):O(k(u,"chat/completions",D(r),this.tokens,this.headers(),g.signal));for await(let m of d)a.report(m)}catch(d){if(!o.isCancellationRequested)throw this.log(`${n.id} failed: ${d instanceof Error?d.message:String(d)}`),d}finally{p.dispose()}}async provideTokenCount(n,e){return E(e)}};function ee(t){let n=c.window.createOutputChannel("Unity Gateway",{log:!0}),e=()=>{let o=c.workspace.getConfiguration("unityGateway");return N(u=>o.get(u))},s=()=>{let o=c.extensions.getExtension("GitHub.copilot-chat")?.packageJSON?.version;return{"User-Agent":`ucode/${b.version} githubcopilotchat${typeof o=="string"?`/${o}`:""}`}},a=new v(e,o=>n.info(o),s);t.subscriptions.push(n,c.lm.registerLanguageModelChatProvider("databricks-unity-gateway",a),c.commands.registerCommand("unityGateway.refreshModels",()=>a.refresh()),c.workspace.onDidChangeConfiguration(o=>{o.affectsConfiguration("unityGateway")&&a.refresh()}))}0&&(module.exports={DatabricksModels,activate,readSettings}); diff --git a/src/ucode/vscode/extension/package.json b/src/ucode/vscode/extension/package.json index 1b2269dd4..e3d7eea65 100644 --- a/src/ucode/vscode/extension/package.json +++ b/src/ucode/vscode/extension/package.json @@ -83,13 +83,12 @@ "scripts": { "build": "esbuild src/extension.ts --bundle --minify --legal-comments=none --outfile=../src/ucode/vscode/extension/out/extension.js --external:vscode --format=cjs --platform=node --target=node22 && node -e \"require('fs').copyFileSync('package.json', '../src/ucode/vscode/extension/package.json')\"", "typecheck": "tsc", - "test": "vitest run" + "test": "esbuild test/*.test.ts --bundle --platform=node --format=cjs --target=node22 --outdir=.test-out --alias:vscode=./test/vscode.ts --log-level=warning && node --test .test-out/*.test.js" }, "devDependencies": { "@types/node": "^22.20.3", "@types/vscode": "1.125.0", "esbuild": "^0.28.2", - "typescript": "^5.9.3", - "vitest": "^4.1.12" + "typescript": "^5.9.3" } } diff --git a/vscode-extension/.gitignore b/vscode-extension/.gitignore index 28a78a7cd..13f124c2b 100644 --- a/vscode-extension/.gitignore +++ b/vscode-extension/.gitignore @@ -1,2 +1,3 @@ node_modules/ *.vsix +.test-out/ diff --git a/vscode-extension/package-lock.json b/vscode-extension/package-lock.json index 5e070f430..dfe196058 100644 --- a/vscode-extension/package-lock.json +++ b/vscode-extension/package-lock.json @@ -11,8 +11,7 @@ "@types/node": "^22.20.3", "@types/vscode": "1.125.0", "esbuild": "^0.28.2", - "typescript": "^5.9.3", - "vitest": "^4.1.12" + "typescript": "^5.9.3" }, "engines": { "vscode": "^1.128.0" @@ -460,1053 +459,63 @@ "node": ">=18" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", - "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@oxc-project/types": { - "version": "0.154.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.154.0.tgz", - "integrity": "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/oxc-project" - } - }, - "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.14.tgz", - "integrity": "sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.14.tgz", - "integrity": "sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.14.tgz", - "integrity": "sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.14.tgz", - "integrity": "sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.14.tgz", - "integrity": "sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.14.tgz", - "integrity": "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.14.tgz", - "integrity": "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.14.tgz", - "integrity": "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.14.tgz", - "integrity": "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.14.tgz", - "integrity": "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.14.tgz", - "integrity": "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.14.tgz", - "integrity": "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.14.tgz", - "integrity": "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.14.tgz", - "integrity": "sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.14.tgz", - "integrity": "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.20.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", - "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/vscode": { - "version": "1.125.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.125.0.tgz", - "integrity": "sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/expect": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.12.tgz", - "integrity": "sha512-vFlTt+YEu9WBpNjdGayNmlMVzrWPi1+sv853/7t2FU7hpO5gAFBKHzQmkCq5Xjpr+evegF2994LQT3hF3mRlqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.12", - "@vitest/utils": "4.1.12", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.12.tgz", - "integrity": "sha512-fKShNpKZzupWZuY0z7egydvL0sqW4INEZBEExMBMAa6izlENEVHwoXgdaTCaBZ5fwlr4xf2o1wXC83n9RpO+NA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.12", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.12.tgz", - "integrity": "sha512-XW9O8+Mxx4VpCtkYdMJLZQQwsVryP9wk7cPKDxtXFfwSak6yRh5+2fep/Y0XF/ElBvJHq8bcYdi6aeuai0opZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.12.tgz", - "integrity": "sha512-NKlVKCr5QnCe8jijdMnmkpHHU+WTqiLFR6G0pHji9uRjXK9xJl1k1nTw6F3pOOj2PDcsDt2GKQMI7wIsaT491w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.12", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.12.tgz", - "integrity": "sha512-ko1y1Qgb/slzQ9mr+b89Cuqud8WG1TKtsPD8r7ZUBdvVOGt85Af2HwCqHujrhwJq95hs/9RM7fcAPg57o1vlzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.12", - "@vitest/utils": "4.1.12", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.12.tgz", - "integrity": "sha512-RGq9x4JNGNTZHEJxvN0oSiJjeSCExm0n2gZKKJZHY9cX5AVA6M7VJILBT3ZJ/tJ/0J9uWT7fETV9/btoi//vSg==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.12.tgz", - "integrity": "sha512-Q3awXrwOfEV+qc3WmO5ONyE0yO7Q1gQ9/oyHxGLZ/pl7N3NO/ZztqQwKLuwMkV+CA1JYSIeO3gkHnRDIo4AVaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.12", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/chai": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", - "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/es-module-lexer": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", - "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", - "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.2", - "@esbuild/android-arm": "0.28.2", - "@esbuild/android-arm64": "0.28.2", - "@esbuild/android-x64": "0.28.2", - "@esbuild/darwin-arm64": "0.28.2", - "@esbuild/darwin-x64": "0.28.2", - "@esbuild/freebsd-arm64": "0.28.2", - "@esbuild/freebsd-x64": "0.28.2", - "@esbuild/linux-arm": "0.28.2", - "@esbuild/linux-arm64": "0.28.2", - "@esbuild/linux-ia32": "0.28.2", - "@esbuild/linux-loong64": "0.28.2", - "@esbuild/linux-mips64el": "0.28.2", - "@esbuild/linux-ppc64": "0.28.2", - "@esbuild/linux-riscv64": "0.28.2", - "@esbuild/linux-s390x": "0.28.2", - "@esbuild/linux-x64": "0.28.2", - "@esbuild/netbsd-arm64": "0.28.2", - "@esbuild/netbsd-x64": "0.28.2", - "@esbuild/openbsd-arm64": "0.28.2", - "@esbuild/openbsd-x64": "0.28.2", - "@esbuild/openharmony-arm64": "0.28.2", - "@esbuild/sunos-x64": "0.28.2", - "@esbuild/win32-arm64": "0.28.2", - "@esbuild/win32-ia32": "0.28.2", - "@esbuild/win32-x64": "0.28.2" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/expect-type": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", - "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/lightningcss": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", - "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.33.0", - "lightningcss-darwin-arm64": "1.33.0", - "lightningcss-darwin-x64": "1.33.0", - "lightningcss-freebsd-x64": "1.33.0", - "lightningcss-linux-arm-gnueabihf": "1.33.0", - "lightningcss-linux-arm64-gnu": "1.33.0", - "lightningcss-linux-arm64-musl": "1.33.0", - "lightningcss-linux-x64-gnu": "1.33.0", - "lightningcss-linux-x64-musl": "1.33.0", - "lightningcss-win32-arm64-msvc": "1.33.0", - "lightningcss-win32-x64-msvc": "1.33.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", - "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", - "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", - "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", - "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", - "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", - "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", - "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", - "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", - "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", - "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", - "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "node_modules/@types/node": { + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/nanoid": { - "version": "3.3.20", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", - "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/obug": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", - "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT", - "engines": { - "node": ">=12.20.0" + "undici-types": "~6.21.0" } }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "node_modules/@types/vscode": { + "version": "1.125.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.125.0.tgz", + "integrity": "sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA==", "dev": true, "license": "MIT" }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", - "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.20", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/rolldown": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.14.tgz", - "integrity": "sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==", + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.154.0", - "@rolldown/pluginutils": "^1.0.0" - }, "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" + "esbuild": "bin/esbuild" }, - "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.14", - "@rolldown/binding-android-arm64": "1.2.14", - "@rolldown/binding-darwin-arm64": "1.2.14", - "@rolldown/binding-darwin-x64": "1.2.14", - "@rolldown/binding-freebsd-x64": "1.2.14", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.14", - "@rolldown/binding-linux-arm64-gnu": "1.2.14", - "@rolldown/binding-linux-arm64-musl": "1.2.14", - "@rolldown/binding-linux-ppc64-gnu": "1.2.14", - "@rolldown/binding-linux-s390x-gnu": "1.2.14", - "@rolldown/binding-linux-x64-gnu": "1.2.14", - "@rolldown/binding-linux-x64-musl": "1.2.14", - "@rolldown/binding-openharmony-arm64": "1.2.14", - "@rolldown/binding-win32-arm64-msvc": "1.2.14", - "@rolldown/binding-win32-x64-msvc": "1.2.14" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/source-map-js": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", - "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", - "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", - "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", - "dev": true, - "license": "MIT", "engines": { "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyrainbow": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.2.0.tgz", - "integrity": "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "node_modules/typescript": { @@ -1529,191 +538,6 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" - }, - "node_modules/vite": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz", - "integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "lightningcss": "^1.33.0", - "picomatch": "^4.0.7", - "postcss": "^8.5.29", - "rolldown": "~1.2.12", - "tinyglobby": "^0.2.17" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.7.1", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vitest": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.12.tgz", - "integrity": "sha512-5eWdz2yb3ajxWF3C8C4TjkXl7FGm3n2E/VlazALrQHzK7nYMSGFnCZto0bbPNl77AwkOZVLk0YnBAejUu0NiWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.1.12", - "@vitest/mocker": "4.1.12", - "@vitest/pretty-format": "4.1.12", - "@vitest/runner": "4.1.12", - "@vitest/snapshot": "4.1.12", - "@vitest/spy": "4.1.12", - "@vitest/utils": "4.1.12", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.12", - "@vitest/browser-preview": "4.1.12", - "@vitest/browser-webdriverio": "^4.1.11", - "@vitest/coverage-istanbul": "4.1.12", - "@vitest/coverage-v8": "4.1.12", - "@vitest/ui": "4.1.12", - "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/coverage-istanbul": { - "optional": true - }, - "@vitest/coverage-v8": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "vite": { - "optional": false - } - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } } } } diff --git a/vscode-extension/package.json b/vscode-extension/package.json index 1b2269dd4..e3d7eea65 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -83,13 +83,12 @@ "scripts": { "build": "esbuild src/extension.ts --bundle --minify --legal-comments=none --outfile=../src/ucode/vscode/extension/out/extension.js --external:vscode --format=cjs --platform=node --target=node22 && node -e \"require('fs').copyFileSync('package.json', '../src/ucode/vscode/extension/package.json')\"", "typecheck": "tsc", - "test": "vitest run" + "test": "esbuild test/*.test.ts --bundle --platform=node --format=cjs --target=node22 --outdir=.test-out --alias:vscode=./test/vscode.ts --log-level=warning && node --test .test-out/*.test.js" }, "devDependencies": { "@types/node": "^22.20.3", "@types/vscode": "1.125.0", "esbuild": "^0.28.2", - "typescript": "^5.9.3", - "vitest": "^4.1.12" + "typescript": "^5.9.3" } } diff --git a/vscode-extension/test/auth.test.ts b/vscode-extension/test/auth.test.ts index 7c12ef1c6..484085c48 100644 --- a/vscode-extension/test/auth.test.ts +++ b/vscode-extension/test/auth.test.ts @@ -1,51 +1,51 @@ -import { describe, expect, it } from "vitest"; +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; import { expiresAt, Tokens } from "../src/auth"; -import { jwt, tokenCommand } from "./support"; +import { jwt, rejectsWith, tokenCommand } from "./support"; describe("Tokens", () => { it("reuses a token until it nears expiry", async () => { const valid = tokenCommand([jwt(3600)]); const tokens = new Tokens(valid.command); - expect(await tokens.get()).toBe(await tokens.get()); - expect(valid.runs()).toBe(1); + assert.equal(await tokens.get(), await tokens.get()); + assert.equal(valid.runs(), 1); const expiring = tokenCommand([jwt(60), jwt(3600)]); const refreshed = new Tokens(expiring.command); - expect(await refreshed.get()).not.toBe(await refreshed.get()); - expect(expiring.runs()).toBe(2); + assert.notEqual(await refreshed.get(), await refreshed.get()); + assert.equal(expiring.runs(), 2); }); it("runs the command once for concurrent requests", async () => { const source = tokenCommand([jwt(3600)]); const tokens = new Tokens(source.command); await Promise.all([tokens.get(), tokens.get(), tokens.get()]); - expect(source.runs()).toBe(1); + assert.equal(source.runs(), 1); }); it("forces a new token after a rejection, but not after a stale one", async () => { const source = tokenCommand(["fresh"], "cached"); const tokens = new Tokens(source.command); - expect(await tokens.get()).toBe("cached"); - expect(await tokens.renew("cached")).toBe("fresh"); - expect(await tokens.renew("cached")).toBe("fresh"); - expect(source.runs()).toBe(2); + assert.equal(await tokens.get(), "cached"); + assert.equal(await tokens.renew("cached"), "fresh"); + assert.equal(await tokens.renew("cached"), "fresh"); + assert.equal(source.runs(), 2); }); it("waits for a fetch in flight before deciding to force one", async () => { const source = tokenCommand(["fresh"], "cached"); const tokens = new Tokens(source.command); - const [first, renewed] = await Promise.all([tokens.get(), tokens.renew("cached")]); - expect([first, renewed]).toEqual(["cached", "fresh"]); - expect(source.runs()).toBe(2); + assert.deepEqual(await Promise.all([tokens.get(), tokens.renew("cached")]), ["cached", "fresh"]); + assert.equal(source.runs(), 2); }); it("reports the command's failure and empty output", async () => { - await expect(new Tokens(`node -e "console.error('sign-in expired'); process.exit(1)"`).get()).rejects.toThrow("sign-in expired"); - await expect(new Tokens(`node -e ""`).get()).rejects.toThrow("printed nothing"); + await rejectsWith(new Tokens(`node -e "console.error('sign-in expired'); process.exit(1)"`).get(), "sign-in expired"); + await rejectsWith(new Tokens(`node -e ""`).get(), "printed nothing"); }); it("reads exp from a JWT and gives other tokens an hour", () => { - expect(expiresAt(`h.${Buffer.from('{"exp":1800000000}').toString("base64url")}.s`)).toBe(1_800_000_000_000); - expect(expiresAt("dapi123") - Date.now()).toBeGreaterThan(59 * 60_000); + assert.equal(expiresAt(`h.${Buffer.from('{"exp":1800000000}').toString("base64url")}.s`), 1_800_000_000_000); + assert.ok(expiresAt("dapi123") - Date.now() > 59 * 60_000); }); }); diff --git a/vscode-extension/test/extension.test.ts b/vscode-extension/test/extension.test.ts index 9cced33c0..a05ca2390 100644 --- a/vscode-extension/test/extension.test.ts +++ b/vscode-extension/test/extension.test.ts @@ -1,10 +1,11 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import assert from "node:assert/strict"; +import { afterEach, describe, it } from "node:test"; import type * as vscode from "vscode"; import packageJson from "../package.json"; import { Tokens } from "../src/auth"; import { DatabricksModels, readSettings, type Settings } from "../src/extension"; import { streamEvents } from "../src/gateway"; -import { collect, message, text, tokenCommand, USER } from "./support"; +import { collect, message, rejectsWith, text, tokenCommand, USER } from "./support"; import { LanguageModelChatToolMode, LanguageModelTextPart } from "./vscode"; const sse = (...data: string[]) => data.map((item) => `data: ${item}\n\n`).join(""); @@ -12,7 +13,7 @@ const sse = (...data: string[]) => data.map((item) => `data: ${item}\n\n`).join( /** Replaces fetch with one that answers each request with the next status and SSE body. */ const stubFetch = (...answers: [number, string][]) => { const requests: { url: string; authorization: string; body: Record }[] = []; - vi.stubGlobal("fetch", async (url: string, init: RequestInit) => { + globalThis.fetch = (async (url: string, init: RequestInit) => { const headers = new Headers(init.headers); requests.push({ url, authorization: headers.get("authorization") ?? "", body: JSON.parse(String(init.body)) }); const [status, body] = answers[Math.min(requests.length - 1, answers.length - 1)] ?? [500, ""]; @@ -20,45 +21,48 @@ const stubFetch = (...answers: [number, string][]) => { const bytes = new TextEncoder().encode(body); const middle = Math.floor(bytes.length / 2); return new Response(new Blob([bytes.slice(0, middle), bytes.slice(middle)]).stream(), { status }); - }); + }) as typeof fetch; return requests; }; +const realFetch = globalThis.fetch; afterEach(() => { - vi.unstubAllGlobals(); + globalThis.fetch = realFetch; }); describe("streamEvents", () => { it("yields each event's JSON until [DONE]", async () => { stubFetch([200, sse('{"a":1}', '{"b":2}', "[DONE]", '{"c":3}')]); const tokens = new Tokens(tokenCommand(["t"]).command); - expect(await collect(streamEvents("https://ws", "responses", {}, tokens, {}, new AbortController().signal))).toEqual([{ a: 1 }, { b: 2 }]); + assert.deepEqual(await collect(streamEvents("https://ws", "responses", {}, tokens, {}, new AbortController().signal)), [{ a: 1 }, { b: 2 }]); }); it("keeps a last event that has no trailing newline", async () => { stubFetch([200, `data: {"a":1}\n\ndata: {"b":2}`]); const tokens = new Tokens(tokenCommand(["t"]).command); - expect(await collect(streamEvents("https://ws", "responses", {}, tokens, {}, new AbortController().signal))).toEqual([{ a: 1 }, { b: 2 }]); + assert.deepEqual(await collect(streamEvents("https://ws", "responses", {}, tokens, {}, new AbortController().signal)), [{ a: 1 }, { b: 2 }]); }); it("retries once with a new token after a 401, then asks the user to sign in", async () => { let requests = stubFetch([401, ""], [200, sse("[DONE]")]); await collect(streamEvents("https://ws", "chat/completions", {}, new Tokens(tokenCommand(["fresh"], "rejected").command), {}, new AbortController().signal)); - expect(requests.map((request) => request.authorization)).toEqual(["Bearer rejected", "Bearer fresh"]); - expect(requests[0]?.url).toBe("https://ws/ai-gateway/mlflow/v1/chat/completions"); + assert.deepEqual(requests.map((request) => request.authorization), ["Bearer rejected", "Bearer fresh"]); + assert.equal(requests[0]?.url, "https://ws/ai-gateway/mlflow/v1/chat/completions"); requests = stubFetch([401, ""]); - await expect( + await rejectsWith( collect(streamEvents("https://ws", "responses", {}, new Tokens(tokenCommand(["a", "b"]).command), {}, new AbortController().signal)), - ).rejects.toThrow("Run `databricks auth login --host https://ws`"); - expect(requests).toHaveLength(2); + "Run `databricks auth login --host https://ws`", + ); + assert.equal(requests.length, 2); }); it("reports other gateway errors with their body", async () => { stubFetch([400, '{"message":"bad tools"}']); - await expect( + await rejectsWith( collect(streamEvents("https://ws", "responses", {}, new Tokens(tokenCommand(["t"]).command), {}, new AbortController().signal)), - ).rejects.toThrow('AI Gateway returned 400: {"message":"bad tools"}'); + 'AI Gateway returned 400: {"message":"bad tools"}', + ); }); }); @@ -66,7 +70,7 @@ describe("settings", () => { // Workspace settings mustn't redirect the token or choose the command that prints it. it("are read from user settings only", () => { const properties = Object.values(packageJson.contributes.configuration.properties); - expect(properties.map((property) => property.scope)).toEqual(["application", "application", "application"]); + assert.deepEqual(properties.map((property) => property.scope), ["application", "application", "application"]); }); }); @@ -77,7 +81,7 @@ describe("readSettings", () => { authCommand: "ug auth-token", models: [{ id: "a", api: "responses" }, { id: "b", api: "messages" }, { api: "responses" }, null, { id: "c", api: "chat_completions" }], }; - expect(readSettings((key) => values[key])).toEqual({ + assert.deepEqual(readSettings((key) => values[key]), { workspace: "https://ws", authCommand: "ug auth-token", models: [ @@ -101,12 +105,15 @@ describe("DatabricksModels", () => { it("lists the configured models, or none with a hint when settings are missing", async () => { const logs: string[] = []; const models = await new DatabricksModels(() => settings("cmd"), (line) => logs.push(line)).provideLanguageModelChatInformation(); - expect(models.map((model) => [model.id, model.family, model.api])).toEqual([ - ["system.ai.claude-sonnet-5-5", "claude-sonnet-5-5", "chat_completions"], - ["system.ai.gpt-6-luna", "gpt-6-luna", "responses"], - ]); - expect(await new DatabricksModels(() => ({ ...settings(""), models: [] }), (line) => logs.push(line)).provideLanguageModelChatInformation()).toEqual([]); - expect(logs).toEqual(["No Databricks models: run `ug configure --agents vscode` to set them up."]); + assert.deepEqual( + models.map((model) => [model.id, model.family, model.api]), + [ + ["system.ai.claude-sonnet-5-5", "claude-sonnet-5-5", "chat_completions"], + ["system.ai.gpt-6-luna", "gpt-6-luna", "responses"], + ], + ); + assert.deepEqual(await new DatabricksModels(() => ({ ...settings(""), models: [] }), (line) => logs.push(line)).provideLanguageModelChatInformation(), []); + assert.deepEqual(logs, ["No Databricks models: run `ug configure --agents vscode` to set them up."]); }); it("sends each model's turns to its API with the User-Agent", async () => { @@ -126,10 +133,13 @@ describe("DatabricksModels", () => { { isCancellationRequested: false, onCancellationRequested: () => ({ dispose() {} }) } as unknown as vscode.CancellationToken, ); } - expect(requests.map((request) => [request.url, request.body.model])).toEqual([ - ["https://ws/ai-gateway/mlflow/v1/chat/completions", "system.ai.claude-sonnet-5-5"], - ["https://ws/ai-gateway/mlflow/v1/responses", "system.ai.gpt-6-luna"], - ]); - expect(reported).toEqual([new LanguageModelTextPart("hello"), new LanguageModelTextPart("hi")]); + assert.deepEqual( + requests.map((request) => [request.url, request.body.model]), + [ + ["https://ws/ai-gateway/mlflow/v1/chat/completions", "system.ai.claude-sonnet-5-5"], + ["https://ws/ai-gateway/mlflow/v1/responses", "system.ai.gpt-6-luna"], + ], + ); + assert.deepEqual(reported, [new LanguageModelTextPart("hello"), new LanguageModelTextPart("hi")]); }); }); diff --git a/vscode-extension/test/formats.test.ts b/vscode-extension/test/formats.test.ts index dff44040a..422d38fc7 100644 --- a/vscode-extension/test/formats.test.ts +++ b/vscode-extension/test/formats.test.ts @@ -1,6 +1,7 @@ -import { describe, expect, it } from "vitest"; +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; import { chatCompletionsReplies, chatCompletionsRequest, estimateTokens, responsesReplies, responsesRequest } from "../src/formats"; -import { ASSISTANT, call, collect, events, image, message, result, SYSTEM, text, USER, weatherTool } from "./support"; +import { ASSISTANT, call, collect, events, image, message, rejectsWith, result, SYSTEM, text, USER, weatherTool } from "./support"; import { LanguageModelDataPart, LanguageModelTextPart, LanguageModelToolCallPart } from "./vscode"; const conversation = [ @@ -13,7 +14,7 @@ const turn = { model: "m", messages: conversation, tools: [weatherTool], toolReq describe("Chat Completions", () => { it("sends the conversation, images and tools", () => { - expect(chatCompletionsRequest(turn)).toEqual({ + assert.deepEqual(chatCompletionsRequest(turn), { model: "m", stream: true, messages: [ @@ -29,9 +30,11 @@ describe("Chat Completions", () => { it("gives a tool without an input schema an empty one, and omits tools when there are none", () => { const noSchema = chatCompletionsRequest({ ...turn, tools: [{ name: "list", description: "List", inputSchema: undefined }], toolRequired: false }); - expect(noSchema).toMatchObject({ tools: [{ function: { parameters: { type: "object", properties: {} } } }] }); - expect(noSchema).not.toHaveProperty("tool_choice"); - expect(chatCompletionsRequest({ ...turn, tools: [] })).not.toHaveProperty("tools"); + assert.deepEqual((noSchema as { tools: unknown[] }).tools, [ + { type: "function", function: { name: "list", description: "List", parameters: { type: "object", properties: {} } } }, + ]); + assert.equal("tool_choice" in noSchema, false); + assert.equal("tools" in chatCompletionsRequest({ ...turn, tools: [] }), false); }); it("streams text and assembles a tool call from its fragments", async () => { @@ -44,14 +47,15 @@ describe("Chat Completions", () => { ), ), ); - expect(replies).toEqual([new LanguageModelTextPart("Checking."), new LanguageModelToolCallPart("c1", "get_weather", { city: "Paris" })]); + assert.deepEqual(replies, [new LanguageModelTextPart("Checking."), new LanguageModelToolCallPart("c1", "get_weather", { city: "Paris" })]); }); it("fails on a length stop and on arguments that aren't an object", async () => { - await expect(collect(chatCompletionsReplies(events({ choices: [{ finish_reason: "length" }] })))).rejects.toThrow("output token limit"); - await expect( + await rejectsWith(collect(chatCompletionsReplies(events({ choices: [{ finish_reason: "length" }] }))), "output token limit"); + await rejectsWith( collect(chatCompletionsReplies(events({ choices: [{ delta: { tool_calls: [{ index: 0, id: "c", function: { name: "t", arguments: "[1]" } }] } }] }))), - ).rejects.toThrow("aren't a JSON object"); + "aren't a JSON object", + ); }); }); @@ -59,14 +63,14 @@ describe("estimateTokens", () => { it("counts text and gives each image a flat cost instead of its bytes", () => { const screenshot = new Uint8Array(400_000); const withImage = message(USER, text("abcd".repeat(10)), result("c1", text("ok"), image(1)), new LanguageModelDataPart(screenshot, "image/png")); - expect(estimateTokens("abcdefgh")).toBe(2); - expect(estimateTokens(withImage)).toBe(11 + 2000); + assert.equal(estimateTokens("abcdefgh"), 2); + assert.equal(estimateTokens(withImage), 11 + 2000); }); }); describe("Responses", () => { it("sends the whole conversation statelessly", () => { - expect(responsesRequest(turn)).toEqual({ + assert.deepEqual(responsesRequest(turn), { model: "m", stream: true, store: false, @@ -93,13 +97,14 @@ describe("Responses", () => { ), ), ); - expect(replies).toEqual([new LanguageModelTextPart("Checking."), new LanguageModelToolCallPart("c1", "get_weather", { city: "Paris" })]); + assert.deepEqual(replies, [new LanguageModelTextPart("Checking."), new LanguageModelToolCallPart("c1", "get_weather", { city: "Paris" })]); }); it("fails on a token-limit stop and on a failed response", async () => { - await expect( + await rejectsWith( collect(responsesReplies(events({ type: "response.incomplete", response: { incomplete_details: { reason: "max_output_tokens" } } }))), - ).rejects.toThrow("output token limit"); - await expect(collect(responsesReplies(events({ type: "response.failed", response: { error: { message: "overloaded" } } })))).rejects.toThrow("overloaded"); + "output token limit", + ); + await rejectsWith(collect(responsesReplies(events({ type: "response.failed", response: { error: { message: "overloaded" } } }))), "overloaded"); }); }); diff --git a/vscode-extension/test/support.ts b/vscode-extension/test/support.ts index 501abb01a..7519d648e 100644 --- a/vscode-extension/test/support.ts +++ b/vscode-extension/test/support.ts @@ -1,3 +1,4 @@ +import assert from "node:assert/strict"; import { mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -52,3 +53,10 @@ export const collect = async (items: AsyncIterable): Promise => { } return out; }; + +/** Asserts that `promise` rejects with an error whose message contains `text`. */ +export const rejectsWith = (promise: Promise, text: string) => + assert.rejects(promise, (error: Error) => { + assert.ok(error.message.includes(text), `expected "${error.message}" to contain "${text}"`); + return true; + }); diff --git a/vscode-extension/vitest.config.ts b/vscode-extension/vitest.config.ts deleted file mode 100644 index bdb97c590..000000000 --- a/vscode-extension/vitest.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { resolve } from "node:path"; -import { defineConfig } from "vitest/config"; - -// Unit tests run outside VS Code, so `vscode` resolves to a small stand-in for the API types we use. -export default defineConfig({ - resolve: { alias: { vscode: resolve(__dirname, "test/vscode.ts") } }, - test: { include: ["test/**/*.test.ts"] }, -}); From 2a6e1afa6403c269318e28decc96082725fc16dd Mon Sep 17 00:00:00 2001 From: Xi Lyu Date: Tue, 27 Oct 2026 16:41:32 +0000 Subject: [PATCH 3/6] Import only the version from the extension's package.json The default import inlined the whole manifest into the bundle, so any package.json edit changed it. The User-Agent is now a small tested function. Co-authored-by: Isaac --- src/ucode/vscode/extension/out/extension.js | 4 ++-- vscode-extension/src/extension.ts | 14 +++++++------- vscode-extension/test/extension.test.ts | 9 ++++++++- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/ucode/vscode/extension/out/extension.js b/src/ucode/vscode/extension/out/extension.js index 4dc8384f1..58febb30a 100644 --- a/src/ucode/vscode/extension/out/extension.js +++ b/src/ucode/vscode/extension/out/extension.js @@ -1,2 +1,2 @@ -"use strict";var J=Object.create;var f=Object.defineProperty;var U=Object.getOwnPropertyDescriptor;var F=Object.getOwnPropertyNames;var B=Object.getPrototypeOf,W=Object.prototype.hasOwnProperty;var H=(t,n)=>{for(var e in n)f(t,e,{get:n[e],enumerable:!0})},C=(t,n,e,s)=>{if(n&&typeof n=="object"||typeof n=="function")for(let a of F(n))!W.call(t,a)&&a!==e&&f(t,a,{get:()=>n[a],enumerable:!(s=U(n,a))||s.enumerable});return t};var x=(t,n,e)=>(e=t!=null?J(B(t)):{},C(n||!t||!t.__esModule?f(e,"default",{value:t,enumerable:!0}):e,t)),K=t=>C(f({},"__esModule",{value:!0}),t);var te={};H(te,{DatabricksModels:()=>v,activate:()=>ee,readSettings:()=>N});module.exports=K(te);var c=x(require("vscode"));var b={name:"unity-gateway",displayName:"Databricks Unity Gateway",description:"Chat with Databricks AI Gateway models in VS Code through Unity Gateway (ug)",version:"0.1.0",publisher:"databricks",repository:{type:"git",url:"https://github.com/databricks/unity-gateway.git",directory:"vscode-extension"},engines:{vscode:"^1.128.0"},categories:["AI","Chat"],extensionKind:["ui"],main:"./out/extension.js",activationEvents:[],contributes:{languageModelChatProviders:[{vendor:"databricks-unity-gateway",displayName:"Databricks"}],configuration:{title:"Databricks Unity Gateway",properties:{"unityGateway.workspace":{type:"string",default:"",description:"Databricks workspace URL, for example https://my-workspace.cloud.databricks.com. Set by `ug configure`.",scope:"application"},"unityGateway.models":{type:"array",default:[],description:"Models to offer in the chat model picker. Set by `ug configure`.",items:{type:"object",required:["id","api"],properties:{id:{type:"string",description:"Model service name, for example system.ai.claude-sonnet-5-5."},api:{type:"string",enum:["chat_completions","responses"],description:"The AI Gateway API the model is called through."}}},scope:"application"},"unityGateway.authCommand":{type:"string",default:"",description:"Command that prints a Databricks access token. Set by `ug configure`.",scope:"application"}}},commands:[{command:"unityGateway.refreshModels",title:"Refresh Models",category:"Unity Gateway"}]},scripts:{build:`esbuild src/extension.ts --bundle --minify --legal-comments=none --outfile=../src/ucode/vscode/extension/out/extension.js --external:vscode --format=cjs --platform=node --target=node22 && node -e "require('fs').copyFileSync('package.json', '../src/ucode/vscode/extension/package.json')"`,typecheck:"tsc",test:"esbuild test/*.test.ts --bundle --platform=node --format=cjs --target=node22 --outdir=.test-out --alias:vscode=./test/vscode.ts --log-level=warning && node --test .test-out/*.test.js"},devDependencies:{"@types/node":"^22.20.3","@types/vscode":"1.125.0",esbuild:"^0.28.2",typescript:"^5.9.3"}};var T=require("node:child_process"),z=5*6e4,V=60*6e4,X=t=>new Promise((n,e)=>{(0,T.exec)(t,{timeout:6e4,windowsHide:!0},(s,a,o)=>{s?e(new Error(`Couldn't get a Databricks token: \`${t}\` failed: ${String(o).trim().slice(0,500)||s.message}`)):n(String(a).trim())})}),Q=t=>{try{let{exp:n}=JSON.parse(Buffer.from(t.split(".")[1]??"","base64url").toString());if(typeof n=="number")return n*1e3}catch{}return Date.now()+V},h=class{constructor(n,e=()=>{}){this.command=n;this.onFetch=e}command;onFetch;cached;pending;get(){return this.cached&&this.cached.expiresAt-z>Date.now()?Promise.resolve(this.cached.value):this.fetch(this.command)}async renew(n){return await this.pending?.catch(()=>{}),this.cached&&this.cached.value!==n?this.cached.value:(this.cached=void 0,this.fetch(`${this.command} --force-refresh`))}fetch(n){return this.pending??=X(n).then(e=>{if(!e)throw new Error(`Couldn't get a Databricks token: \`${n}\` printed nothing.`);return this.cached={value:e,expiresAt:Q(e)},this.onFetch(this.cached.expiresAt),e}).finally(()=>{this.pending=void 0}),this.pending}};var i=x(require("vscode")),_=3,Z=1e3,y=t=>t instanceof i.LanguageModelTextPart?t.value:t instanceof i.LanguageModelDataPart&&!t.mimeType.startsWith("image/")?new TextDecoder().decode(t.data):t instanceof i.LanguageModelPromptTsxPart?JSON.stringify(t.value):"",S=t=>t instanceof i.LanguageModelDataPart&&t.mimeType.startsWith("image/")?`data:${t.mimeType};base64,${Buffer.from(t.data).toString("base64")}`:void 0,R=t=>t.inputSchema??{type:"object",properties:{}},L=(t,n,e)=>{let s;try{s=e.trim()?JSON.parse(e):{}}catch{s=void 0}if(typeof s!="object"||s===null||Array.isArray(s))throw new Error(`The model called ${n} with arguments that aren't a JSON object: ${e}`);return new i.LanguageModelToolCallPart(t,n,s)},P=()=>new Error("The model stopped at its output token limit before finishing the response."),A=(t,n)=>t.tools.length?{tools:t.tools.map(n),...t.toolRequired?{tool_choice:"required"}:{}}:{},E=t=>{if(typeof t=="string")return Math.ceil(t.length/4);let n=0,e=0;for(let s of t.content){let a=s instanceof i.LanguageModelToolResultPart?s.content:[s];for(let o of a)n+=o instanceof i.LanguageModelToolCallPart?o.name.length+JSON.stringify(o.input).length:y(o).length,e+=S(o)===void 0?0:1}return Math.ceil(n/4)+e*Z},j=t=>({results:t.filter(n=>n instanceof i.LanguageModelToolResultPart),calls:t.filter(n=>n instanceof i.LanguageModelToolCallPart),text:t.map(y).join(""),images:t.map(S).filter(n=>n!==void 0)});function D(t){let n=t.messages.flatMap(e=>{let{results:s,calls:a,text:o,images:u}=j(e.content);if(Number(e.role)===_)return[{role:"system",content:o}];if(e.role===i.LanguageModelChatMessageRole.Assistant){let r=a.map(g=>({id:g.callId,type:"function",function:{name:g.name,arguments:JSON.stringify(g.input)}}));return o||r.length?[{role:"assistant",content:o||null,...r.length?{tool_calls:r}:{}}]:[]}let l=[...o?[{type:"text",text:o}]:[],...u.map(r=>({type:"image_url",image_url:{url:r}}))];return[...s.map(r=>({role:"tool",tool_call_id:r.callId,content:r.content.map(y).join("")})),...l.length?[{role:"user",content:l}]:[]]});return{model:t.model,messages:n,stream:!0,...A(t,e=>({type:"function",function:{name:e.name,description:e.description,parameters:R(e)}}))}}async function*O(t){let n=[];for await(let e of t)for(let s of e.choices??[]){s.delta?.content&&(yield new i.LanguageModelTextPart(s.delta.content));for(let a of s.delta?.tool_calls??[]){let o=n[a.index??0]??={id:"",name:"",args:""};o.id=a.id||o.id,o.name=a.function?.name||o.name,o.args+=a.function?.arguments??""}if(s.finish_reason==="length")throw P()}for(let e of n.filter(Boolean))yield L(e.id,e.name,e.args)}function I(t){let n=t.messages.flatMap(e=>{let{results:s,calls:a,text:o,images:u}=j(e.content);if(Number(e.role)===_)return[{role:"system",content:o}];if(e.role===i.LanguageModelChatMessageRole.Assistant)return[...o?[{role:"assistant",content:o}]:[],...a.map(r=>({type:"function_call",call_id:r.callId,name:r.name,arguments:JSON.stringify(r.input)}))];let l=[...o?[{type:"input_text",text:o}]:[],...u.map(r=>({type:"input_image",image_url:r,detail:"auto"}))];return[...s.map(r=>({type:"function_call_output",call_id:r.callId,output:r.content.map(y).join("")})),...l.length?[{role:"user",content:l}]:[]]});return{model:t.model,input:n,stream:!0,store:!1,...A(t,e=>({type:"function",name:e.name,description:e.description,parameters:R(e),strict:!1}))}}async function*G(t){for await(let n of t){let e=n;if(e.type==="response.output_text.delta"&&e.delta)yield new i.LanguageModelTextPart(e.delta);else if(e.type==="response.output_item.done"&&e.item?.type==="function_call")yield L(e.item.call_id??"",e.item.name??"",e.item.arguments??"");else{if(e.type==="response.incomplete"&&e.response?.incomplete_details?.reason==="max_output_tokens")throw P();if(e.type==="response.failed"||e.type==="error")throw new Error(e.response?.error?.message??e.message??"The model response failed.")}}}async function*k(t,n,e,s,a,o){let u=m=>fetch(`${t}/ai-gateway/mlflow/v1/${n}`,{method:"POST",headers:{...a,Authorization:`Bearer ${m}`,"Content-Type":"application/json"},body:JSON.stringify(e),signal:o}),l=await s.get(),r=await u(l);if(r.status===401&&(await r.body?.cancel(),l=await s.renew(l),r=await u(l)),r.status===401)throw await r.body?.cancel(),new Error(`Databricks rejected the sign-in. Run \`databricks auth login --host ${t}\`, then retry.`);if(!r.ok||!r.body)throw new Error(`AI Gateway returned ${r.status}: ${(await r.text()).slice(0,300)}`);let g=new TextDecoder,p="";for await(let m of r.body){p+=g.decode(m,{stream:!0});let M=p.split(` -`);p=M.pop()??"";for(let q of M){let w=$(q);if(w==="[DONE]")return;w&&(yield JSON.parse(w))}}let d=$(p+g.decode());d&&d!=="[DONE]"&&(yield JSON.parse(d))}var $=t=>t.startsWith("data:")?t.slice(5).trim():"";var N=t=>{let n=t("models");return{workspace:String(t("workspace")??"").replace(/\/+$/,""),models:(Array.isArray(n)?n:[]).filter(e=>typeof e?.id=="string"&&e.id!==""&&(e.api==="chat_completions"||e.api==="responses")),authCommand:String(t("authCommand")??"")}},v=class{constructor(n,e,s=()=>({})){this.settings=n;this.log=e;this.headers=s}settings;log;headers;changed=new c.EventEmitter;onDidChangeLanguageModelChatInformation=this.changed.event;tokens;refresh(){this.tokens=void 0,this.changed.fire()}async provideLanguageModelChatInformation(){let{workspace:n,models:e,authCommand:s}=this.settings();return!n||!s||e.length===0?(this.log("No Databricks models: run `ug configure --agents vscode` to set them up."),[]):e.map(({id:a,api:o})=>({id:a,api:o,name:a,family:a.split(".").pop()??a,version:b.version,detail:"Databricks",maxInputTokens:128e3,maxOutputTokens:32e3,capabilities:{toolCalling:!0,imageInput:o==="responses"||a.includes("claude")}}))}async provideLanguageModelChatResponse(n,e,s,a,o){let{workspace:u,authCommand:l}=this.settings();this.tokens??=new h(l,d=>this.log(`Fetched a Databricks token (expires ${new Date(d).toISOString()})`));let r={model:n.id,messages:e,tools:s.tools??[],toolRequired:s.toolMode===c.LanguageModelChatToolMode.Required},g=new AbortController,p=o.onCancellationRequested(()=>g.abort());try{let d=n.api==="responses"?G(k(u,"responses",I(r),this.tokens,this.headers(),g.signal)):O(k(u,"chat/completions",D(r),this.tokens,this.headers(),g.signal));for await(let m of d)a.report(m)}catch(d){if(!o.isCancellationRequested)throw this.log(`${n.id} failed: ${d instanceof Error?d.message:String(d)}`),d}finally{p.dispose()}}async provideTokenCount(n,e){return E(e)}};function ee(t){let n=c.window.createOutputChannel("Unity Gateway",{log:!0}),e=()=>{let o=c.workspace.getConfiguration("unityGateway");return N(u=>o.get(u))},s=()=>{let o=c.extensions.getExtension("GitHub.copilot-chat")?.packageJSON?.version;return{"User-Agent":`ucode/${b.version} githubcopilotchat${typeof o=="string"?`/${o}`:""}`}},a=new v(e,o=>n.info(o),s);t.subscriptions.push(n,c.lm.registerLanguageModelChatProvider("databricks-unity-gateway",a),c.commands.registerCommand("unityGateway.refreshModels",()=>a.refresh()),c.workspace.onDidChangeConfiguration(o=>{o.affectsConfiguration("unityGateway")&&a.refresh()}))}0&&(module.exports={DatabricksModels,activate,readSettings}); +"use strict";var U=Object.create;var f=Object.defineProperty;var F=Object.getOwnPropertyDescriptor;var B=Object.getOwnPropertyNames;var W=Object.getPrototypeOf,H=Object.prototype.hasOwnProperty;var K=(e,n)=>{for(var t in n)f(e,t,{get:n[t],enumerable:!0})},x=(e,n,t,s)=>{if(n&&typeof n=="object"||typeof n=="function")for(let a of B(n))!H.call(e,a)&&a!==t&&f(e,a,{get:()=>n[a],enumerable:!(s=F(n,a))||s.enumerable});return e};var C=(e,n,t)=>(t=e!=null?U(W(e)):{},x(n||!e||!e.__esModule?f(t,"default",{value:e,enumerable:!0}):t,e)),Y=e=>x(f({},"__esModule",{value:!0}),e);var ne={};K(ne,{DatabricksModels:()=>v,activate:()=>te,readSettings:()=>N,userAgent:()=>q});module.exports=Y(ne);var c=C(require("vscode"));var b="0.1.0";var T=require("node:child_process"),X=5*6e4,Q=60*6e4,V=e=>new Promise((n,t)=>{(0,T.exec)(e,{timeout:6e4,windowsHide:!0},(s,a,o)=>{s?t(new Error(`Couldn't get a Databricks token: \`${e}\` failed: ${String(o).trim().slice(0,500)||s.message}`)):n(String(a).trim())})}),Z=e=>{try{let{exp:n}=JSON.parse(Buffer.from(e.split(".")[1]??"","base64url").toString());if(typeof n=="number")return n*1e3}catch{}return Date.now()+Q},h=class{constructor(n,t=()=>{}){this.command=n;this.onFetch=t}command;onFetch;cached;pending;get(){return this.cached&&this.cached.expiresAt-X>Date.now()?Promise.resolve(this.cached.value):this.fetch(this.command)}async renew(n){return await this.pending?.catch(()=>{}),this.cached&&this.cached.value!==n?this.cached.value:(this.cached=void 0,this.fetch(`${this.command} --force-refresh`))}fetch(n){return this.pending??=V(n).then(t=>{if(!t)throw new Error(`Couldn't get a Databricks token: \`${n}\` printed nothing.`);return this.cached={value:t,expiresAt:Z(t)},this.onFetch(this.cached.expiresAt),t}).finally(()=>{this.pending=void 0}),this.pending}};var i=C(require("vscode")),_=3,ee=1e3,y=e=>e instanceof i.LanguageModelTextPart?e.value:e instanceof i.LanguageModelDataPart&&!e.mimeType.startsWith("image/")?new TextDecoder().decode(e.data):e instanceof i.LanguageModelPromptTsxPart?JSON.stringify(e.value):"",S=e=>e instanceof i.LanguageModelDataPart&&e.mimeType.startsWith("image/")?`data:${e.mimeType};base64,${Buffer.from(e.data).toString("base64")}`:void 0,R=e=>e.inputSchema??{type:"object",properties:{}},L=(e,n,t)=>{let s;try{s=t.trim()?JSON.parse(t):{}}catch{s=void 0}if(typeof s!="object"||s===null||Array.isArray(s))throw new Error(`The model called ${n} with arguments that aren't a JSON object: ${t}`);return new i.LanguageModelToolCallPart(e,n,s)},P=()=>new Error("The model stopped at its output token limit before finishing the response."),A=(e,n)=>e.tools.length?{tools:e.tools.map(n),...e.toolRequired?{tool_choice:"required"}:{}}:{},E=e=>{if(typeof e=="string")return Math.ceil(e.length/4);let n=0,t=0;for(let s of e.content){let a=s instanceof i.LanguageModelToolResultPart?s.content:[s];for(let o of a)n+=o instanceof i.LanguageModelToolCallPart?o.name.length+JSON.stringify(o.input).length:y(o).length,t+=S(o)===void 0?0:1}return Math.ceil(n/4)+t*ee},j=e=>({results:e.filter(n=>n instanceof i.LanguageModelToolResultPart),calls:e.filter(n=>n instanceof i.LanguageModelToolCallPart),text:e.map(y).join(""),images:e.map(S).filter(n=>n!==void 0)});function D(e){let n=e.messages.flatMap(t=>{let{results:s,calls:a,text:o,images:u}=j(t.content);if(Number(t.role)===_)return[{role:"system",content:o}];if(t.role===i.LanguageModelChatMessageRole.Assistant){let r=a.map(g=>({id:g.callId,type:"function",function:{name:g.name,arguments:JSON.stringify(g.input)}}));return o||r.length?[{role:"assistant",content:o||null,...r.length?{tool_calls:r}:{}}]:[]}let l=[...o?[{type:"text",text:o}]:[],...u.map(r=>({type:"image_url",image_url:{url:r}}))];return[...s.map(r=>({role:"tool",tool_call_id:r.callId,content:r.content.map(y).join("")})),...l.length?[{role:"user",content:l}]:[]]});return{model:e.model,messages:n,stream:!0,...A(e,t=>({type:"function",function:{name:t.name,description:t.description,parameters:R(t)}}))}}async function*O(e){let n=[];for await(let t of e)for(let s of t.choices??[]){s.delta?.content&&(yield new i.LanguageModelTextPart(s.delta.content));for(let a of s.delta?.tool_calls??[]){let o=n[a.index??0]??={id:"",name:"",args:""};o.id=a.id||o.id,o.name=a.function?.name||o.name,o.args+=a.function?.arguments??""}if(s.finish_reason==="length")throw P()}for(let t of n.filter(Boolean))yield L(t.id,t.name,t.args)}function I(e){let n=e.messages.flatMap(t=>{let{results:s,calls:a,text:o,images:u}=j(t.content);if(Number(t.role)===_)return[{role:"system",content:o}];if(t.role===i.LanguageModelChatMessageRole.Assistant)return[...o?[{role:"assistant",content:o}]:[],...a.map(r=>({type:"function_call",call_id:r.callId,name:r.name,arguments:JSON.stringify(r.input)}))];let l=[...o?[{type:"input_text",text:o}]:[],...u.map(r=>({type:"input_image",image_url:r,detail:"auto"}))];return[...s.map(r=>({type:"function_call_output",call_id:r.callId,output:r.content.map(y).join("")})),...l.length?[{role:"user",content:l}]:[]]});return{model:e.model,input:n,stream:!0,store:!1,...A(e,t=>({type:"function",name:t.name,description:t.description,parameters:R(t),strict:!1}))}}async function*G(e){for await(let n of e){let t=n;if(t.type==="response.output_text.delta"&&t.delta)yield new i.LanguageModelTextPart(t.delta);else if(t.type==="response.output_item.done"&&t.item?.type==="function_call")yield L(t.item.call_id??"",t.item.name??"",t.item.arguments??"");else{if(t.type==="response.incomplete"&&t.response?.incomplete_details?.reason==="max_output_tokens")throw P();if(t.type==="response.failed"||t.type==="error")throw new Error(t.response?.error?.message??t.message??"The model response failed.")}}}async function*k(e,n,t,s,a,o){let u=m=>fetch(`${e}/ai-gateway/mlflow/v1/${n}`,{method:"POST",headers:{...a,Authorization:`Bearer ${m}`,"Content-Type":"application/json"},body:JSON.stringify(t),signal:o}),l=await s.get(),r=await u(l);if(r.status===401&&(await r.body?.cancel(),l=await s.renew(l),r=await u(l)),r.status===401)throw await r.body?.cancel(),new Error(`Databricks rejected the sign-in. Run \`databricks auth login --host ${e}\`, then retry.`);if(!r.ok||!r.body)throw new Error(`AI Gateway returned ${r.status}: ${(await r.text()).slice(0,300)}`);let g=new TextDecoder,p="";for await(let m of r.body){p+=g.decode(m,{stream:!0});let M=p.split(` +`);p=M.pop()??"";for(let J of M){let w=$(J);if(w==="[DONE]")return;w&&(yield JSON.parse(w))}}let d=$(p+g.decode());d&&d!=="[DONE]"&&(yield JSON.parse(d))}var $=e=>e.startsWith("data:")?e.slice(5).trim():"";var N=e=>{let n=e("models");return{workspace:String(e("workspace")??"").replace(/\/+$/,""),models:(Array.isArray(n)?n:[]).filter(t=>typeof t?.id=="string"&&t.id!==""&&(t.api==="chat_completions"||t.api==="responses")),authCommand:String(e("authCommand")??"")}},q=e=>`ucode/${b} githubcopilotchat${typeof e=="string"?`/${e}`:""}`,v=class{constructor(n,t,s=()=>({})){this.settings=n;this.log=t;this.headers=s}settings;log;headers;changed=new c.EventEmitter;onDidChangeLanguageModelChatInformation=this.changed.event;tokens;refresh(){this.tokens=void 0,this.changed.fire()}async provideLanguageModelChatInformation(){let{workspace:n,models:t,authCommand:s}=this.settings();return!n||!s||t.length===0?(this.log("No Databricks models: run `ug configure --agents vscode` to set them up."),[]):t.map(({id:a,api:o})=>({id:a,api:o,name:a,family:a.split(".").pop()??a,version:b,detail:"Databricks",maxInputTokens:128e3,maxOutputTokens:32e3,capabilities:{toolCalling:!0,imageInput:o==="responses"||a.includes("claude")}}))}async provideLanguageModelChatResponse(n,t,s,a,o){let{workspace:u,authCommand:l}=this.settings();this.tokens??=new h(l,d=>this.log(`Fetched a Databricks token (expires ${new Date(d).toISOString()})`));let r={model:n.id,messages:t,tools:s.tools??[],toolRequired:s.toolMode===c.LanguageModelChatToolMode.Required},g=new AbortController,p=o.onCancellationRequested(()=>g.abort());try{let d=n.api==="responses"?G(k(u,"responses",I(r),this.tokens,this.headers(),g.signal)):O(k(u,"chat/completions",D(r),this.tokens,this.headers(),g.signal));for await(let m of d)a.report(m)}catch(d){if(!o.isCancellationRequested)throw this.log(`${n.id} failed: ${d instanceof Error?d.message:String(d)}`),d}finally{p.dispose()}}async provideTokenCount(n,t){return E(t)}};function te(e){let n=c.window.createOutputChannel("Unity Gateway",{log:!0}),t=()=>{let o=c.workspace.getConfiguration("unityGateway");return N(u=>o.get(u))},s=()=>({"User-Agent":q(c.extensions.getExtension("GitHub.copilot-chat")?.packageJSON?.version)}),a=new v(t,o=>n.info(o),s);e.subscriptions.push(n,c.lm.registerLanguageModelChatProvider("databricks-unity-gateway",a),c.commands.registerCommand("unityGateway.refreshModels",()=>a.refresh()),c.workspace.onDidChangeConfiguration(o=>{o.affectsConfiguration("unityGateway")&&a.refresh()}))}0&&(module.exports={DatabricksModels,activate,readSettings,userAgent}); diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index 1ccd159bb..8d1bf42df 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -1,5 +1,5 @@ import * as vscode from "vscode"; -import packageJson from "../package.json"; +import { version } from "../package.json"; import { Tokens } from "./auth"; import { chatCompletionsReplies, chatCompletionsRequest, estimateTokens, responsesReplies, responsesRequest, type Reply } from "./formats"; import { streamEvents } from "./gateway"; @@ -27,6 +27,10 @@ export const readSettings = (get: (key: string) => unknown): Settings => { }; }; +/** The gateway attributes requests by this User-Agent. */ +export const userAgent = (copilotChatVersion: unknown): string => + `ucode/${version} githubcopilotchat${typeof copilotChatVersion === "string" ? `/${copilotChatVersion}` : ""}`; + type Model = vscode.LanguageModelChatInformation & { readonly api: ModelSetting["api"] }; export class DatabricksModels implements vscode.LanguageModelChatProvider { @@ -58,7 +62,7 @@ export class DatabricksModels implements vscode.LanguageModelChatProvider name: id, // Copilot Chat tunes its prompts and tools by family, so this is the model's own name. family: id.split(".").pop() ?? id, - version: packageJson.version, + version, detail: "Databricks", // The gateway reports no limits for these models. maxInputTokens: 128_000, @@ -108,11 +112,7 @@ export function activate(context: vscode.ExtensionContext): void { const configuration = vscode.workspace.getConfiguration("unityGateway"); return readSettings((key) => configuration.get(key)); }; - // The gateway attributes requests by this User-Agent. - const headers = () => { - const version: unknown = vscode.extensions.getExtension("GitHub.copilot-chat")?.packageJSON?.version; - return { "User-Agent": `ucode/${packageJson.version} githubcopilotchat${typeof version === "string" ? `/${version}` : ""}` }; - }; + const headers = () => ({ "User-Agent": userAgent(vscode.extensions.getExtension("GitHub.copilot-chat")?.packageJSON?.version) }); const provider = new DatabricksModels(settings, (line) => output.info(line), headers); context.subscriptions.push( output, diff --git a/vscode-extension/test/extension.test.ts b/vscode-extension/test/extension.test.ts index a05ca2390..ce1e8f9c4 100644 --- a/vscode-extension/test/extension.test.ts +++ b/vscode-extension/test/extension.test.ts @@ -3,7 +3,7 @@ import { afterEach, describe, it } from "node:test"; import type * as vscode from "vscode"; import packageJson from "../package.json"; import { Tokens } from "../src/auth"; -import { DatabricksModels, readSettings, type Settings } from "../src/extension"; +import { DatabricksModels, readSettings, userAgent, type Settings } from "../src/extension"; import { streamEvents } from "../src/gateway"; import { collect, message, rejectsWith, text, tokenCommand, USER } from "./support"; import { LanguageModelChatToolMode, LanguageModelTextPart } from "./vscode"; @@ -74,6 +74,13 @@ describe("settings", () => { }); }); +describe("userAgent", () => { + it("names ug's extension and Copilot Chat's version", () => { + assert.equal(userAgent("0.71.0"), `ucode/${packageJson.version} githubcopilotchat/0.71.0`); + assert.equal(userAgent(undefined), `ucode/${packageJson.version} githubcopilotchat`); + }); +}); + describe("readSettings", () => { it("keeps well-formed models and trims the workspace URL", () => { const values: Record = { From 7926b0432bdfbdf15150be14bcf7f28ff1043012 Mon Sep 17 00:00:00 2001 From: Xi Lyu Date: Tue, 27 Oct 2026 18:38:11 +0000 Subject: [PATCH 4/6] Send only the text blocks of a thinking model's stream While a Claude model thinks, the AI Gateway's Chat Completions stream sends `delta.content` as a list of blocks instead of a string: {"choices":[{"delta":{"content":[{"type":"reasoning","summary":[...]}]}}]} The extension passed that list straight into a `LanguageModelTextPart`, and Copilot Chat's agent loop then failed the whole turn with "Cannot read properties of undefined (reading 'length')" before the model's tool call ran. Whether a model thinks depends on the prompt, so the same model would answer some requests and fail others. Take the text out of the blocks instead, and drop the reasoning. Co-authored-by: Isaac --- src/ucode/vscode/extension/out/extension.js | 4 ++-- vscode-extension/src/formats.ts | 19 ++++++++++++++++--- vscode-extension/test/formats.test.ts | 12 ++++++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/ucode/vscode/extension/out/extension.js b/src/ucode/vscode/extension/out/extension.js index 58febb30a..3980ddbdf 100644 --- a/src/ucode/vscode/extension/out/extension.js +++ b/src/ucode/vscode/extension/out/extension.js @@ -1,2 +1,2 @@ -"use strict";var U=Object.create;var f=Object.defineProperty;var F=Object.getOwnPropertyDescriptor;var B=Object.getOwnPropertyNames;var W=Object.getPrototypeOf,H=Object.prototype.hasOwnProperty;var K=(e,n)=>{for(var t in n)f(e,t,{get:n[t],enumerable:!0})},x=(e,n,t,s)=>{if(n&&typeof n=="object"||typeof n=="function")for(let a of B(n))!H.call(e,a)&&a!==t&&f(e,a,{get:()=>n[a],enumerable:!(s=F(n,a))||s.enumerable});return e};var C=(e,n,t)=>(t=e!=null?U(W(e)):{},x(n||!e||!e.__esModule?f(t,"default",{value:e,enumerable:!0}):t,e)),Y=e=>x(f({},"__esModule",{value:!0}),e);var ne={};K(ne,{DatabricksModels:()=>v,activate:()=>te,readSettings:()=>N,userAgent:()=>q});module.exports=Y(ne);var c=C(require("vscode"));var b="0.1.0";var T=require("node:child_process"),X=5*6e4,Q=60*6e4,V=e=>new Promise((n,t)=>{(0,T.exec)(e,{timeout:6e4,windowsHide:!0},(s,a,o)=>{s?t(new Error(`Couldn't get a Databricks token: \`${e}\` failed: ${String(o).trim().slice(0,500)||s.message}`)):n(String(a).trim())})}),Z=e=>{try{let{exp:n}=JSON.parse(Buffer.from(e.split(".")[1]??"","base64url").toString());if(typeof n=="number")return n*1e3}catch{}return Date.now()+Q},h=class{constructor(n,t=()=>{}){this.command=n;this.onFetch=t}command;onFetch;cached;pending;get(){return this.cached&&this.cached.expiresAt-X>Date.now()?Promise.resolve(this.cached.value):this.fetch(this.command)}async renew(n){return await this.pending?.catch(()=>{}),this.cached&&this.cached.value!==n?this.cached.value:(this.cached=void 0,this.fetch(`${this.command} --force-refresh`))}fetch(n){return this.pending??=V(n).then(t=>{if(!t)throw new Error(`Couldn't get a Databricks token: \`${n}\` printed nothing.`);return this.cached={value:t,expiresAt:Z(t)},this.onFetch(this.cached.expiresAt),t}).finally(()=>{this.pending=void 0}),this.pending}};var i=C(require("vscode")),_=3,ee=1e3,y=e=>e instanceof i.LanguageModelTextPart?e.value:e instanceof i.LanguageModelDataPart&&!e.mimeType.startsWith("image/")?new TextDecoder().decode(e.data):e instanceof i.LanguageModelPromptTsxPart?JSON.stringify(e.value):"",S=e=>e instanceof i.LanguageModelDataPart&&e.mimeType.startsWith("image/")?`data:${e.mimeType};base64,${Buffer.from(e.data).toString("base64")}`:void 0,R=e=>e.inputSchema??{type:"object",properties:{}},L=(e,n,t)=>{let s;try{s=t.trim()?JSON.parse(t):{}}catch{s=void 0}if(typeof s!="object"||s===null||Array.isArray(s))throw new Error(`The model called ${n} with arguments that aren't a JSON object: ${t}`);return new i.LanguageModelToolCallPart(e,n,s)},P=()=>new Error("The model stopped at its output token limit before finishing the response."),A=(e,n)=>e.tools.length?{tools:e.tools.map(n),...e.toolRequired?{tool_choice:"required"}:{}}:{},E=e=>{if(typeof e=="string")return Math.ceil(e.length/4);let n=0,t=0;for(let s of e.content){let a=s instanceof i.LanguageModelToolResultPart?s.content:[s];for(let o of a)n+=o instanceof i.LanguageModelToolCallPart?o.name.length+JSON.stringify(o.input).length:y(o).length,t+=S(o)===void 0?0:1}return Math.ceil(n/4)+t*ee},j=e=>({results:e.filter(n=>n instanceof i.LanguageModelToolResultPart),calls:e.filter(n=>n instanceof i.LanguageModelToolCallPart),text:e.map(y).join(""),images:e.map(S).filter(n=>n!==void 0)});function D(e){let n=e.messages.flatMap(t=>{let{results:s,calls:a,text:o,images:u}=j(t.content);if(Number(t.role)===_)return[{role:"system",content:o}];if(t.role===i.LanguageModelChatMessageRole.Assistant){let r=a.map(g=>({id:g.callId,type:"function",function:{name:g.name,arguments:JSON.stringify(g.input)}}));return o||r.length?[{role:"assistant",content:o||null,...r.length?{tool_calls:r}:{}}]:[]}let l=[...o?[{type:"text",text:o}]:[],...u.map(r=>({type:"image_url",image_url:{url:r}}))];return[...s.map(r=>({role:"tool",tool_call_id:r.callId,content:r.content.map(y).join("")})),...l.length?[{role:"user",content:l}]:[]]});return{model:e.model,messages:n,stream:!0,...A(e,t=>({type:"function",function:{name:t.name,description:t.description,parameters:R(t)}}))}}async function*O(e){let n=[];for await(let t of e)for(let s of t.choices??[]){s.delta?.content&&(yield new i.LanguageModelTextPart(s.delta.content));for(let a of s.delta?.tool_calls??[]){let o=n[a.index??0]??={id:"",name:"",args:""};o.id=a.id||o.id,o.name=a.function?.name||o.name,o.args+=a.function?.arguments??""}if(s.finish_reason==="length")throw P()}for(let t of n.filter(Boolean))yield L(t.id,t.name,t.args)}function I(e){let n=e.messages.flatMap(t=>{let{results:s,calls:a,text:o,images:u}=j(t.content);if(Number(t.role)===_)return[{role:"system",content:o}];if(t.role===i.LanguageModelChatMessageRole.Assistant)return[...o?[{role:"assistant",content:o}]:[],...a.map(r=>({type:"function_call",call_id:r.callId,name:r.name,arguments:JSON.stringify(r.input)}))];let l=[...o?[{type:"input_text",text:o}]:[],...u.map(r=>({type:"input_image",image_url:r,detail:"auto"}))];return[...s.map(r=>({type:"function_call_output",call_id:r.callId,output:r.content.map(y).join("")})),...l.length?[{role:"user",content:l}]:[]]});return{model:e.model,input:n,stream:!0,store:!1,...A(e,t=>({type:"function",name:t.name,description:t.description,parameters:R(t),strict:!1}))}}async function*G(e){for await(let n of e){let t=n;if(t.type==="response.output_text.delta"&&t.delta)yield new i.LanguageModelTextPart(t.delta);else if(t.type==="response.output_item.done"&&t.item?.type==="function_call")yield L(t.item.call_id??"",t.item.name??"",t.item.arguments??"");else{if(t.type==="response.incomplete"&&t.response?.incomplete_details?.reason==="max_output_tokens")throw P();if(t.type==="response.failed"||t.type==="error")throw new Error(t.response?.error?.message??t.message??"The model response failed.")}}}async function*k(e,n,t,s,a,o){let u=m=>fetch(`${e}/ai-gateway/mlflow/v1/${n}`,{method:"POST",headers:{...a,Authorization:`Bearer ${m}`,"Content-Type":"application/json"},body:JSON.stringify(t),signal:o}),l=await s.get(),r=await u(l);if(r.status===401&&(await r.body?.cancel(),l=await s.renew(l),r=await u(l)),r.status===401)throw await r.body?.cancel(),new Error(`Databricks rejected the sign-in. Run \`databricks auth login --host ${e}\`, then retry.`);if(!r.ok||!r.body)throw new Error(`AI Gateway returned ${r.status}: ${(await r.text()).slice(0,300)}`);let g=new TextDecoder,p="";for await(let m of r.body){p+=g.decode(m,{stream:!0});let M=p.split(` -`);p=M.pop()??"";for(let J of M){let w=$(J);if(w==="[DONE]")return;w&&(yield JSON.parse(w))}}let d=$(p+g.decode());d&&d!=="[DONE]"&&(yield JSON.parse(d))}var $=e=>e.startsWith("data:")?e.slice(5).trim():"";var N=e=>{let n=e("models");return{workspace:String(e("workspace")??"").replace(/\/+$/,""),models:(Array.isArray(n)?n:[]).filter(t=>typeof t?.id=="string"&&t.id!==""&&(t.api==="chat_completions"||t.api==="responses")),authCommand:String(e("authCommand")??"")}},q=e=>`ucode/${b} githubcopilotchat${typeof e=="string"?`/${e}`:""}`,v=class{constructor(n,t,s=()=>({})){this.settings=n;this.log=t;this.headers=s}settings;log;headers;changed=new c.EventEmitter;onDidChangeLanguageModelChatInformation=this.changed.event;tokens;refresh(){this.tokens=void 0,this.changed.fire()}async provideLanguageModelChatInformation(){let{workspace:n,models:t,authCommand:s}=this.settings();return!n||!s||t.length===0?(this.log("No Databricks models: run `ug configure --agents vscode` to set them up."),[]):t.map(({id:a,api:o})=>({id:a,api:o,name:a,family:a.split(".").pop()??a,version:b,detail:"Databricks",maxInputTokens:128e3,maxOutputTokens:32e3,capabilities:{toolCalling:!0,imageInput:o==="responses"||a.includes("claude")}}))}async provideLanguageModelChatResponse(n,t,s,a,o){let{workspace:u,authCommand:l}=this.settings();this.tokens??=new h(l,d=>this.log(`Fetched a Databricks token (expires ${new Date(d).toISOString()})`));let r={model:n.id,messages:t,tools:s.tools??[],toolRequired:s.toolMode===c.LanguageModelChatToolMode.Required},g=new AbortController,p=o.onCancellationRequested(()=>g.abort());try{let d=n.api==="responses"?G(k(u,"responses",I(r),this.tokens,this.headers(),g.signal)):O(k(u,"chat/completions",D(r),this.tokens,this.headers(),g.signal));for await(let m of d)a.report(m)}catch(d){if(!o.isCancellationRequested)throw this.log(`${n.id} failed: ${d instanceof Error?d.message:String(d)}`),d}finally{p.dispose()}}async provideTokenCount(n,t){return E(t)}};function te(e){let n=c.window.createOutputChannel("Unity Gateway",{log:!0}),t=()=>{let o=c.workspace.getConfiguration("unityGateway");return N(u=>o.get(u))},s=()=>({"User-Agent":q(c.extensions.getExtension("GitHub.copilot-chat")?.packageJSON?.version)}),a=new v(t,o=>n.info(o),s);e.subscriptions.push(n,c.lm.registerLanguageModelChatProvider("databricks-unity-gateway",a),c.commands.registerCommand("unityGateway.refreshModels",()=>a.refresh()),c.workspace.onDidChangeConfiguration(o=>{o.affectsConfiguration("unityGateway")&&a.refresh()}))}0&&(module.exports={DatabricksModels,activate,readSettings,userAgent}); +"use strict";var U=Object.create;var f=Object.defineProperty;var F=Object.getOwnPropertyDescriptor;var B=Object.getOwnPropertyNames;var W=Object.getPrototypeOf,H=Object.prototype.hasOwnProperty;var K=(e,n)=>{for(var t in n)f(e,t,{get:n[t],enumerable:!0})},x=(e,n,t,s)=>{if(n&&typeof n=="object"||typeof n=="function")for(let r of B(n))!H.call(e,r)&&r!==t&&f(e,r,{get:()=>n[r],enumerable:!(s=F(n,r))||s.enumerable});return e};var M=(e,n,t)=>(t=e!=null?U(W(e)):{},x(n||!e||!e.__esModule?f(t,"default",{value:e,enumerable:!0}):t,e)),Y=e=>x(f({},"__esModule",{value:!0}),e);var oe={};K(oe,{DatabricksModels:()=>v,activate:()=>ne,readSettings:()=>N,userAgent:()=>q});module.exports=Y(oe);var l=M(require("vscode"));var b="0.1.0";var T=require("node:child_process"),X=5*6e4,Q=60*6e4,V=e=>new Promise((n,t)=>{(0,T.exec)(e,{timeout:6e4,windowsHide:!0},(s,r,o)=>{s?t(new Error(`Couldn't get a Databricks token: \`${e}\` failed: ${String(o).trim().slice(0,500)||s.message}`)):n(String(r).trim())})}),Z=e=>{try{let{exp:n}=JSON.parse(Buffer.from(e.split(".")[1]??"","base64url").toString());if(typeof n=="number")return n*1e3}catch{}return Date.now()+Q},h=class{constructor(n,t=()=>{}){this.command=n;this.onFetch=t}command;onFetch;cached;pending;get(){return this.cached&&this.cached.expiresAt-X>Date.now()?Promise.resolve(this.cached.value):this.fetch(this.command)}async renew(n){return await this.pending?.catch(()=>{}),this.cached&&this.cached.value!==n?this.cached.value:(this.cached=void 0,this.fetch(`${this.command} --force-refresh`))}fetch(n){return this.pending??=V(n).then(t=>{if(!t)throw new Error(`Couldn't get a Databricks token: \`${n}\` printed nothing.`);return this.cached={value:t,expiresAt:Z(t)},this.onFetch(this.cached.expiresAt),t}).finally(()=>{this.pending=void 0}),this.pending}};var i=M(require("vscode")),_=3,ee=1e3,y=e=>e instanceof i.LanguageModelTextPart?e.value:e instanceof i.LanguageModelDataPart&&!e.mimeType.startsWith("image/")?new TextDecoder().decode(e.data):e instanceof i.LanguageModelPromptTsxPart?JSON.stringify(e.value):"",S=e=>e instanceof i.LanguageModelDataPart&&e.mimeType.startsWith("image/")?`data:${e.mimeType};base64,${Buffer.from(e.data).toString("base64")}`:void 0,R=e=>e.inputSchema??{type:"object",properties:{}},L=(e,n,t)=>{let s;try{s=t.trim()?JSON.parse(t):{}}catch{s=void 0}if(typeof s!="object"||s===null||Array.isArray(s))throw new Error(`The model called ${n} with arguments that aren't a JSON object: ${t}`);return new i.LanguageModelToolCallPart(e,n,s)},P=()=>new Error("The model stopped at its output token limit before finishing the response."),A=(e,n)=>e.tools.length?{tools:e.tools.map(n),...e.toolRequired?{tool_choice:"required"}:{}}:{},E=e=>{if(typeof e=="string")return Math.ceil(e.length/4);let n=0,t=0;for(let s of e.content){let r=s instanceof i.LanguageModelToolResultPart?s.content:[s];for(let o of r)n+=o instanceof i.LanguageModelToolCallPart?o.name.length+JSON.stringify(o.input).length:y(o).length,t+=S(o)===void 0?0:1}return Math.ceil(n/4)+t*ee},j=e=>({results:e.filter(n=>n instanceof i.LanguageModelToolResultPart),calls:e.filter(n=>n instanceof i.LanguageModelToolCallPart),text:e.map(y).join(""),images:e.map(S).filter(n=>n!==void 0)});function D(e){let n=e.messages.flatMap(t=>{let{results:s,calls:r,text:o,images:c}=j(t.content);if(Number(t.role)===_)return[{role:"system",content:o}];if(t.role===i.LanguageModelChatMessageRole.Assistant){let a=r.map(g=>({id:g.callId,type:"function",function:{name:g.name,arguments:JSON.stringify(g.input)}}));return o||a.length?[{role:"assistant",content:o||null,...a.length?{tool_calls:a}:{}}]:[]}let d=[...o?[{type:"text",text:o}]:[],...c.map(a=>({type:"image_url",image_url:{url:a}}))];return[...s.map(a=>({role:"tool",tool_call_id:a.callId,content:a.content.map(y).join("")})),...d.length?[{role:"user",content:d}]:[]]});return{model:e.model,messages:n,stream:!0,...A(e,t=>({type:"function",function:{name:t.name,description:t.description,parameters:R(t)}}))}}var te=e=>typeof e=="string"?e:(e??[]).filter(n=>n.type==="text").map(n=>n.text??"").join("");async function*O(e){let n=[];for await(let t of e)for(let s of t.choices??[]){let r=te(s.delta?.content);r&&(yield new i.LanguageModelTextPart(r));for(let o of s.delta?.tool_calls??[]){let c=n[o.index??0]??={id:"",name:"",args:""};c.id=o.id||c.id,c.name=o.function?.name||c.name,c.args+=o.function?.arguments??""}if(s.finish_reason==="length")throw P()}for(let t of n.filter(Boolean))yield L(t.id,t.name,t.args)}function I(e){let n=e.messages.flatMap(t=>{let{results:s,calls:r,text:o,images:c}=j(t.content);if(Number(t.role)===_)return[{role:"system",content:o}];if(t.role===i.LanguageModelChatMessageRole.Assistant)return[...o?[{role:"assistant",content:o}]:[],...r.map(a=>({type:"function_call",call_id:a.callId,name:a.name,arguments:JSON.stringify(a.input)}))];let d=[...o?[{type:"input_text",text:o}]:[],...c.map(a=>({type:"input_image",image_url:a,detail:"auto"}))];return[...s.map(a=>({type:"function_call_output",call_id:a.callId,output:a.content.map(y).join("")})),...d.length?[{role:"user",content:d}]:[]]});return{model:e.model,input:n,stream:!0,store:!1,...A(e,t=>({type:"function",name:t.name,description:t.description,parameters:R(t),strict:!1}))}}async function*G(e){for await(let n of e){let t=n;if(t.type==="response.output_text.delta"&&t.delta)yield new i.LanguageModelTextPart(t.delta);else if(t.type==="response.output_item.done"&&t.item?.type==="function_call")yield L(t.item.call_id??"",t.item.name??"",t.item.arguments??"");else{if(t.type==="response.incomplete"&&t.response?.incomplete_details?.reason==="max_output_tokens")throw P();if(t.type==="response.failed"||t.type==="error")throw new Error(t.response?.error?.message??t.message??"The model response failed.")}}}async function*C(e,n,t,s,r,o){let c=m=>fetch(`${e}/ai-gateway/mlflow/v1/${n}`,{method:"POST",headers:{...r,Authorization:`Bearer ${m}`,"Content-Type":"application/json"},body:JSON.stringify(t),signal:o}),d=await s.get(),a=await c(d);if(a.status===401&&(await a.body?.cancel(),d=await s.renew(d),a=await c(d)),a.status===401)throw await a.body?.cancel(),new Error(`Databricks rejected the sign-in. Run \`databricks auth login --host ${e}\`, then retry.`);if(!a.ok||!a.body)throw new Error(`AI Gateway returned ${a.status}: ${(await a.text()).slice(0,300)}`);let g=new TextDecoder,p="";for await(let m of a.body){p+=g.decode(m,{stream:!0});let k=p.split(` +`);p=k.pop()??"";for(let J of k){let w=$(J);if(w==="[DONE]")return;w&&(yield JSON.parse(w))}}let u=$(p+g.decode());u&&u!=="[DONE]"&&(yield JSON.parse(u))}var $=e=>e.startsWith("data:")?e.slice(5).trim():"";var N=e=>{let n=e("models");return{workspace:String(e("workspace")??"").replace(/\/+$/,""),models:(Array.isArray(n)?n:[]).filter(t=>typeof t?.id=="string"&&t.id!==""&&(t.api==="chat_completions"||t.api==="responses")),authCommand:String(e("authCommand")??"")}},q=e=>`ucode/${b} githubcopilotchat${typeof e=="string"?`/${e}`:""}`,v=class{constructor(n,t,s=()=>({})){this.settings=n;this.log=t;this.headers=s}settings;log;headers;changed=new l.EventEmitter;onDidChangeLanguageModelChatInformation=this.changed.event;tokens;refresh(){this.tokens=void 0,this.changed.fire()}async provideLanguageModelChatInformation(){let{workspace:n,models:t,authCommand:s}=this.settings();return!n||!s||t.length===0?(this.log("No Databricks models: run `ug configure --agents vscode` to set them up."),[]):t.map(({id:r,api:o})=>({id:r,api:o,name:r,family:r.split(".").pop()??r,version:b,detail:"Databricks",maxInputTokens:128e3,maxOutputTokens:32e3,capabilities:{toolCalling:!0,imageInput:o==="responses"||r.includes("claude")}}))}async provideLanguageModelChatResponse(n,t,s,r,o){let{workspace:c,authCommand:d}=this.settings();this.tokens??=new h(d,u=>this.log(`Fetched a Databricks token (expires ${new Date(u).toISOString()})`));let a={model:n.id,messages:t,tools:s.tools??[],toolRequired:s.toolMode===l.LanguageModelChatToolMode.Required},g=new AbortController,p=o.onCancellationRequested(()=>g.abort());try{let u=n.api==="responses"?G(C(c,"responses",I(a),this.tokens,this.headers(),g.signal)):O(C(c,"chat/completions",D(a),this.tokens,this.headers(),g.signal));for await(let m of u)r.report(m)}catch(u){if(!o.isCancellationRequested)throw this.log(`${n.id} failed: ${u instanceof Error?u.message:String(u)}`),u}finally{p.dispose()}}async provideTokenCount(n,t){return E(t)}};function ne(e){let n=l.window.createOutputChannel("Unity Gateway",{log:!0}),t=()=>{let o=l.workspace.getConfiguration("unityGateway");return N(c=>o.get(c))},s=()=>({"User-Agent":q(l.extensions.getExtension("GitHub.copilot-chat")?.packageJSON?.version)}),r=new v(t,o=>n.info(o),s);e.subscriptions.push(n,l.lm.registerLanguageModelChatProvider("databricks-unity-gateway",r),l.commands.registerCommand("unityGateway.refreshModels",()=>r.refresh()),l.workspace.onDidChangeConfiguration(o=>{o.affectsConfiguration("unityGateway")&&r.refresh()}))}0&&(module.exports={DatabricksModels,activate,readSettings,userAgent}); diff --git a/vscode-extension/src/formats.ts b/vscode-extension/src/formats.ts index 7de8819f0..d2c61f7e9 100644 --- a/vscode-extension/src/formats.ts +++ b/vscode-extension/src/formats.ts @@ -109,19 +109,32 @@ export function chatCompletionsRequest(turn: Turn): object { }; } +type ChatContent = string | { type?: string; text?: string }[] | null; + interface ChatChunk { choices?: { - delta?: { content?: string | null; tool_calls?: { index?: number; id?: string; function?: { name?: string; arguments?: string } }[] }; + delta?: { content?: ChatContent; tool_calls?: { index?: number; id?: string; function?: { name?: string; arguments?: string } }[] }; finish_reason?: string | null; }[]; } +/** While a Claude model thinks, the gateway sends `content` as blocks rather than a string. Only the + * text ones are the reply: passing a block along would make Copilot Chat fail the turn. */ +const deltaText = (content: ChatContent | undefined): string => + typeof content === "string" + ? content + : (content ?? []) + .filter((block) => block.type === "text") + .map((block) => block.text ?? "") + .join(""); + export async function* chatCompletionsReplies(events: AsyncIterable): AsyncGenerator { const calls: { id: string; name: string; args: string }[] = []; for await (const event of events) { for (const choice of (event as ChatChunk).choices ?? []) { - if (choice.delta?.content) { - yield new vscode.LanguageModelTextPart(choice.delta.content); + const text = deltaText(choice.delta?.content); + if (text) { + yield new vscode.LanguageModelTextPart(text); } for (const fragment of choice.delta?.tool_calls ?? []) { const call = (calls[fragment.index ?? 0] ??= { id: "", name: "", args: "" }); diff --git a/vscode-extension/test/formats.test.ts b/vscode-extension/test/formats.test.ts index 422d38fc7..9f66d8817 100644 --- a/vscode-extension/test/formats.test.ts +++ b/vscode-extension/test/formats.test.ts @@ -50,6 +50,18 @@ describe("Chat Completions", () => { assert.deepEqual(replies, [new LanguageModelTextPart("Checking."), new LanguageModelToolCallPart("c1", "get_weather", { city: "Paris" })]); }); + it("takes the text out of content blocks and drops a thinking model's reasoning", async () => { + const replies = await collect( + chatCompletionsReplies( + events( + { choices: [{ delta: { content: [{ type: "reasoning", summary: [{ type: "summary_text", text: "The file may not exist." }] }] } }] }, + { choices: [{ delta: { content: [{ type: "text", text: "Checking." }, { type: "text", text: " Done." }] } }] }, + ), + ), + ); + assert.deepEqual(replies, [new LanguageModelTextPart("Checking. Done.")]); + }); + it("fails on a length stop and on arguments that aren't an object", async () => { await rejectsWith(collect(chatCompletionsReplies(events({ choices: [{ finish_reason: "length" }] }))), "output token limit"); await rejectsWith( From 4eeb2fad56a9e88b93987e301c93202954e42b0f Mon Sep 17 00:00:00 2001 From: Xi Lyu Date: Wed, 28 Oct 2026 14:00:30 +0000 Subject: [PATCH 5/6] Show the Databricks logo for the extension's models The chat model picker and Manage Language Models showed VS Code's generic icon next to the Databricks provider. Give the provider the Databricks logo: one SVG in the brand red, which reads on light and dark themes. The build copies it next to the bundle so ug ships it. Co-authored-by: Isaac --- src/ucode/vscode/extension/media/databricks.svg | 1 + src/ucode/vscode/extension/out/extension.js | 4 ++-- src/ucode/vscode/extension/package.json | 5 +++-- vscode-extension/media/databricks.svg | 1 + vscode-extension/package.json | 5 +++-- vscode-extension/test/extension.test.ts | 7 +++++++ 6 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 src/ucode/vscode/extension/media/databricks.svg create mode 100644 vscode-extension/media/databricks.svg diff --git a/src/ucode/vscode/extension/media/databricks.svg b/src/ucode/vscode/extension/media/databricks.svg new file mode 100644 index 000000000..b7bd91e12 --- /dev/null +++ b/src/ucode/vscode/extension/media/databricks.svg @@ -0,0 +1 @@ + diff --git a/src/ucode/vscode/extension/out/extension.js b/src/ucode/vscode/extension/out/extension.js index 3980ddbdf..6a51bc56a 100644 --- a/src/ucode/vscode/extension/out/extension.js +++ b/src/ucode/vscode/extension/out/extension.js @@ -1,2 +1,2 @@ -"use strict";var U=Object.create;var f=Object.defineProperty;var F=Object.getOwnPropertyDescriptor;var B=Object.getOwnPropertyNames;var W=Object.getPrototypeOf,H=Object.prototype.hasOwnProperty;var K=(e,n)=>{for(var t in n)f(e,t,{get:n[t],enumerable:!0})},x=(e,n,t,s)=>{if(n&&typeof n=="object"||typeof n=="function")for(let r of B(n))!H.call(e,r)&&r!==t&&f(e,r,{get:()=>n[r],enumerable:!(s=F(n,r))||s.enumerable});return e};var M=(e,n,t)=>(t=e!=null?U(W(e)):{},x(n||!e||!e.__esModule?f(t,"default",{value:e,enumerable:!0}):t,e)),Y=e=>x(f({},"__esModule",{value:!0}),e);var oe={};K(oe,{DatabricksModels:()=>v,activate:()=>ne,readSettings:()=>N,userAgent:()=>q});module.exports=Y(oe);var l=M(require("vscode"));var b="0.1.0";var T=require("node:child_process"),X=5*6e4,Q=60*6e4,V=e=>new Promise((n,t)=>{(0,T.exec)(e,{timeout:6e4,windowsHide:!0},(s,r,o)=>{s?t(new Error(`Couldn't get a Databricks token: \`${e}\` failed: ${String(o).trim().slice(0,500)||s.message}`)):n(String(r).trim())})}),Z=e=>{try{let{exp:n}=JSON.parse(Buffer.from(e.split(".")[1]??"","base64url").toString());if(typeof n=="number")return n*1e3}catch{}return Date.now()+Q},h=class{constructor(n,t=()=>{}){this.command=n;this.onFetch=t}command;onFetch;cached;pending;get(){return this.cached&&this.cached.expiresAt-X>Date.now()?Promise.resolve(this.cached.value):this.fetch(this.command)}async renew(n){return await this.pending?.catch(()=>{}),this.cached&&this.cached.value!==n?this.cached.value:(this.cached=void 0,this.fetch(`${this.command} --force-refresh`))}fetch(n){return this.pending??=V(n).then(t=>{if(!t)throw new Error(`Couldn't get a Databricks token: \`${n}\` printed nothing.`);return this.cached={value:t,expiresAt:Z(t)},this.onFetch(this.cached.expiresAt),t}).finally(()=>{this.pending=void 0}),this.pending}};var i=M(require("vscode")),_=3,ee=1e3,y=e=>e instanceof i.LanguageModelTextPart?e.value:e instanceof i.LanguageModelDataPart&&!e.mimeType.startsWith("image/")?new TextDecoder().decode(e.data):e instanceof i.LanguageModelPromptTsxPart?JSON.stringify(e.value):"",S=e=>e instanceof i.LanguageModelDataPart&&e.mimeType.startsWith("image/")?`data:${e.mimeType};base64,${Buffer.from(e.data).toString("base64")}`:void 0,R=e=>e.inputSchema??{type:"object",properties:{}},L=(e,n,t)=>{let s;try{s=t.trim()?JSON.parse(t):{}}catch{s=void 0}if(typeof s!="object"||s===null||Array.isArray(s))throw new Error(`The model called ${n} with arguments that aren't a JSON object: ${t}`);return new i.LanguageModelToolCallPart(e,n,s)},P=()=>new Error("The model stopped at its output token limit before finishing the response."),A=(e,n)=>e.tools.length?{tools:e.tools.map(n),...e.toolRequired?{tool_choice:"required"}:{}}:{},E=e=>{if(typeof e=="string")return Math.ceil(e.length/4);let n=0,t=0;for(let s of e.content){let r=s instanceof i.LanguageModelToolResultPart?s.content:[s];for(let o of r)n+=o instanceof i.LanguageModelToolCallPart?o.name.length+JSON.stringify(o.input).length:y(o).length,t+=S(o)===void 0?0:1}return Math.ceil(n/4)+t*ee},j=e=>({results:e.filter(n=>n instanceof i.LanguageModelToolResultPart),calls:e.filter(n=>n instanceof i.LanguageModelToolCallPart),text:e.map(y).join(""),images:e.map(S).filter(n=>n!==void 0)});function D(e){let n=e.messages.flatMap(t=>{let{results:s,calls:r,text:o,images:c}=j(t.content);if(Number(t.role)===_)return[{role:"system",content:o}];if(t.role===i.LanguageModelChatMessageRole.Assistant){let a=r.map(g=>({id:g.callId,type:"function",function:{name:g.name,arguments:JSON.stringify(g.input)}}));return o||a.length?[{role:"assistant",content:o||null,...a.length?{tool_calls:a}:{}}]:[]}let d=[...o?[{type:"text",text:o}]:[],...c.map(a=>({type:"image_url",image_url:{url:a}}))];return[...s.map(a=>({role:"tool",tool_call_id:a.callId,content:a.content.map(y).join("")})),...d.length?[{role:"user",content:d}]:[]]});return{model:e.model,messages:n,stream:!0,...A(e,t=>({type:"function",function:{name:t.name,description:t.description,parameters:R(t)}}))}}var te=e=>typeof e=="string"?e:(e??[]).filter(n=>n.type==="text").map(n=>n.text??"").join("");async function*O(e){let n=[];for await(let t of e)for(let s of t.choices??[]){let r=te(s.delta?.content);r&&(yield new i.LanguageModelTextPart(r));for(let o of s.delta?.tool_calls??[]){let c=n[o.index??0]??={id:"",name:"",args:""};c.id=o.id||c.id,c.name=o.function?.name||c.name,c.args+=o.function?.arguments??""}if(s.finish_reason==="length")throw P()}for(let t of n.filter(Boolean))yield L(t.id,t.name,t.args)}function I(e){let n=e.messages.flatMap(t=>{let{results:s,calls:r,text:o,images:c}=j(t.content);if(Number(t.role)===_)return[{role:"system",content:o}];if(t.role===i.LanguageModelChatMessageRole.Assistant)return[...o?[{role:"assistant",content:o}]:[],...r.map(a=>({type:"function_call",call_id:a.callId,name:a.name,arguments:JSON.stringify(a.input)}))];let d=[...o?[{type:"input_text",text:o}]:[],...c.map(a=>({type:"input_image",image_url:a,detail:"auto"}))];return[...s.map(a=>({type:"function_call_output",call_id:a.callId,output:a.content.map(y).join("")})),...d.length?[{role:"user",content:d}]:[]]});return{model:e.model,input:n,stream:!0,store:!1,...A(e,t=>({type:"function",name:t.name,description:t.description,parameters:R(t),strict:!1}))}}async function*G(e){for await(let n of e){let t=n;if(t.type==="response.output_text.delta"&&t.delta)yield new i.LanguageModelTextPart(t.delta);else if(t.type==="response.output_item.done"&&t.item?.type==="function_call")yield L(t.item.call_id??"",t.item.name??"",t.item.arguments??"");else{if(t.type==="response.incomplete"&&t.response?.incomplete_details?.reason==="max_output_tokens")throw P();if(t.type==="response.failed"||t.type==="error")throw new Error(t.response?.error?.message??t.message??"The model response failed.")}}}async function*C(e,n,t,s,r,o){let c=m=>fetch(`${e}/ai-gateway/mlflow/v1/${n}`,{method:"POST",headers:{...r,Authorization:`Bearer ${m}`,"Content-Type":"application/json"},body:JSON.stringify(t),signal:o}),d=await s.get(),a=await c(d);if(a.status===401&&(await a.body?.cancel(),d=await s.renew(d),a=await c(d)),a.status===401)throw await a.body?.cancel(),new Error(`Databricks rejected the sign-in. Run \`databricks auth login --host ${e}\`, then retry.`);if(!a.ok||!a.body)throw new Error(`AI Gateway returned ${a.status}: ${(await a.text()).slice(0,300)}`);let g=new TextDecoder,p="";for await(let m of a.body){p+=g.decode(m,{stream:!0});let k=p.split(` -`);p=k.pop()??"";for(let J of k){let w=$(J);if(w==="[DONE]")return;w&&(yield JSON.parse(w))}}let u=$(p+g.decode());u&&u!=="[DONE]"&&(yield JSON.parse(u))}var $=e=>e.startsWith("data:")?e.slice(5).trim():"";var N=e=>{let n=e("models");return{workspace:String(e("workspace")??"").replace(/\/+$/,""),models:(Array.isArray(n)?n:[]).filter(t=>typeof t?.id=="string"&&t.id!==""&&(t.api==="chat_completions"||t.api==="responses")),authCommand:String(e("authCommand")??"")}},q=e=>`ucode/${b} githubcopilotchat${typeof e=="string"?`/${e}`:""}`,v=class{constructor(n,t,s=()=>({})){this.settings=n;this.log=t;this.headers=s}settings;log;headers;changed=new l.EventEmitter;onDidChangeLanguageModelChatInformation=this.changed.event;tokens;refresh(){this.tokens=void 0,this.changed.fire()}async provideLanguageModelChatInformation(){let{workspace:n,models:t,authCommand:s}=this.settings();return!n||!s||t.length===0?(this.log("No Databricks models: run `ug configure --agents vscode` to set them up."),[]):t.map(({id:r,api:o})=>({id:r,api:o,name:r,family:r.split(".").pop()??r,version:b,detail:"Databricks",maxInputTokens:128e3,maxOutputTokens:32e3,capabilities:{toolCalling:!0,imageInput:o==="responses"||r.includes("claude")}}))}async provideLanguageModelChatResponse(n,t,s,r,o){let{workspace:c,authCommand:d}=this.settings();this.tokens??=new h(d,u=>this.log(`Fetched a Databricks token (expires ${new Date(u).toISOString()})`));let a={model:n.id,messages:t,tools:s.tools??[],toolRequired:s.toolMode===l.LanguageModelChatToolMode.Required},g=new AbortController,p=o.onCancellationRequested(()=>g.abort());try{let u=n.api==="responses"?G(C(c,"responses",I(a),this.tokens,this.headers(),g.signal)):O(C(c,"chat/completions",D(a),this.tokens,this.headers(),g.signal));for await(let m of u)r.report(m)}catch(u){if(!o.isCancellationRequested)throw this.log(`${n.id} failed: ${u instanceof Error?u.message:String(u)}`),u}finally{p.dispose()}}async provideTokenCount(n,t){return E(t)}};function ne(e){let n=l.window.createOutputChannel("Unity Gateway",{log:!0}),t=()=>{let o=l.workspace.getConfiguration("unityGateway");return N(c=>o.get(c))},s=()=>({"User-Agent":q(l.extensions.getExtension("GitHub.copilot-chat")?.packageJSON?.version)}),r=new v(t,o=>n.info(o),s);e.subscriptions.push(n,l.lm.registerLanguageModelChatProvider("databricks-unity-gateway",r),l.commands.registerCommand("unityGateway.refreshModels",()=>r.refresh()),l.workspace.onDidChangeConfiguration(o=>{o.affectsConfiguration("unityGateway")&&r.refresh()}))}0&&(module.exports={DatabricksModels,activate,readSettings,userAgent}); +"use strict";var U=Object.create;var f=Object.defineProperty;var F=Object.getOwnPropertyDescriptor;var B=Object.getOwnPropertyNames;var W=Object.getPrototypeOf,H=Object.prototype.hasOwnProperty;var K=(e,n)=>{for(var t in n)f(e,t,{get:n[t],enumerable:!0})},C=(e,n,t,s)=>{if(n&&typeof n=="object"||typeof n=="function")for(let i of B(n))!H.call(e,i)&&i!==t&&f(e,i,{get:()=>n[i],enumerable:!(s=F(n,i))||s.enumerable});return e};var M=(e,n,t)=>(t=e!=null?U(W(e)):{},C(n||!e||!e.__esModule?f(t,"default",{value:e,enumerable:!0}):t,e)),Y=e=>C(f({},"__esModule",{value:!0}),e);var oe={};K(oe,{DatabricksModels:()=>v,activate:()=>ne,readSettings:()=>N,userAgent:()=>q});module.exports=Y(oe);var l=M(require("vscode"));var b="0.1.0";var T=require("node:child_process"),X=5*6e4,Q=60*6e4,V=e=>new Promise((n,t)=>{(0,T.exec)(e,{timeout:6e4,windowsHide:!0},(s,i,o)=>{s?t(new Error(`Couldn't get a Databricks token: \`${e}\` failed: ${String(o).trim().slice(0,500)||s.message}`)):n(String(i).trim())})}),Z=e=>{try{let{exp:n}=JSON.parse(Buffer.from(e.split(".")[1]??"","base64url").toString());if(typeof n=="number")return n*1e3}catch{}return Date.now()+Q},h=class{constructor(n,t=()=>{}){this.command=n;this.onFetch=t}command;onFetch;cached;pending;get(){return this.cached&&this.cached.expiresAt-X>Date.now()?Promise.resolve(this.cached.value):this.fetch(this.command)}async renew(n){return await this.pending?.catch(()=>{}),this.cached&&this.cached.value!==n?this.cached.value:(this.cached=void 0,this.fetch(`${this.command} --force-refresh`))}fetch(n){return this.pending??=V(n).then(t=>{if(!t)throw new Error(`Couldn't get a Databricks token: \`${n}\` printed nothing.`);return this.cached={value:t,expiresAt:Z(t)},this.onFetch(this.cached.expiresAt),t}).finally(()=>{this.pending=void 0}),this.pending}};var r=M(require("vscode")),S=3,ee=1e3,y=e=>e instanceof r.LanguageModelTextPart?e.value:e instanceof r.LanguageModelDataPart&&!e.mimeType.startsWith("image/")?new TextDecoder().decode(e.data):e instanceof r.LanguageModelPromptTsxPart?JSON.stringify(e.value):"",_=e=>e instanceof r.LanguageModelDataPart&&e.mimeType.startsWith("image/")?`data:${e.mimeType};base64,${Buffer.from(e.data).toString("base64")}`:void 0,R=e=>e.inputSchema??{type:"object",properties:{}},L=(e,n,t)=>{let s;try{s=t.trim()?JSON.parse(t):{}}catch{s=void 0}if(typeof s!="object"||s===null||Array.isArray(s))throw new Error(`The model called ${n} with arguments that aren't a JSON object: ${t}`);return new r.LanguageModelToolCallPart(e,n,s)},P=()=>new Error("The model stopped at its output token limit before finishing the response."),A=(e,n)=>e.tools.length?{tools:e.tools.map(n),...e.toolRequired?{tool_choice:"required"}:{}}:{},E=e=>{if(typeof e=="string")return Math.ceil(e.length/4);let n=0,t=0;for(let s of e.content){let i=s instanceof r.LanguageModelToolResultPart?s.content:[s];for(let o of i)n+=o instanceof r.LanguageModelToolCallPart?o.name.length+JSON.stringify(o.input).length:y(o).length,t+=_(o)===void 0?0:1}return Math.ceil(n/4)+t*ee},j=e=>({results:e.filter(n=>n instanceof r.LanguageModelToolResultPart),calls:e.filter(n=>n instanceof r.LanguageModelToolCallPart),text:e.map(y).join(""),images:e.map(_).filter(n=>n!==void 0)});function D(e){let n=e.messages.flatMap(t=>{let{results:s,calls:i,text:o,images:c}=j(t.content);if(Number(t.role)===S)return[{role:"system",content:o}];if(t.role===r.LanguageModelChatMessageRole.Assistant){let a=i.map(g=>({id:g.callId,type:"function",function:{name:g.name,arguments:JSON.stringify(g.input)}}));return o||a.length?[{role:"assistant",content:o||null,...a.length?{tool_calls:a}:{}}]:[]}let d=[...o?[{type:"text",text:o}]:[],...c.map(a=>({type:"image_url",image_url:{url:a}}))];return[...s.map(a=>({role:"tool",tool_call_id:a.callId,content:a.content.map(y).join("")})),...d.length?[{role:"user",content:d}]:[]]});return{model:e.model,messages:n,stream:!0,...A(e,t=>({type:"function",function:{name:t.name,description:t.description,parameters:R(t)}}))}}var te=e=>typeof e=="string"?e:(e??[]).filter(n=>n.type==="text").map(n=>n.text??"").join("");async function*O(e){let n=[];for await(let t of e)for(let s of t.choices??[]){let i=te(s.delta?.content);i&&(yield new r.LanguageModelTextPart(i));for(let o of s.delta?.tool_calls??[]){let c=n[o.index??0]??={id:"",name:"",args:""};c.id=o.id||c.id,c.name=o.function?.name||c.name,c.args+=o.function?.arguments??""}if(s.finish_reason==="length")throw P()}for(let t of n.filter(Boolean))yield L(t.id,t.name,t.args)}function I(e){let n=e.messages.flatMap(t=>{let{results:s,calls:i,text:o,images:c}=j(t.content);if(Number(t.role)===S)return[{role:"system",content:o}];if(t.role===r.LanguageModelChatMessageRole.Assistant)return[...o?[{role:"assistant",content:o}]:[],...i.map(a=>({type:"function_call",call_id:a.callId,name:a.name,arguments:JSON.stringify(a.input)}))];let d=[...o?[{type:"input_text",text:o}]:[],...c.map(a=>({type:"input_image",image_url:a,detail:"auto"}))];return[...s.map(a=>({type:"function_call_output",call_id:a.callId,output:a.content.map(y).join("")})),...d.length?[{role:"user",content:d}]:[]]});return{model:e.model,input:n,stream:!0,store:!1,...A(e,t=>({type:"function",name:t.name,description:t.description,parameters:R(t),strict:!1}))}}async function*G(e){for await(let n of e){let t=n;if(t.type==="response.output_text.delta"&&t.delta)yield new r.LanguageModelTextPart(t.delta);else if(t.type==="response.output_item.done"&&t.item?.type==="function_call")yield L(t.item.call_id??"",t.item.name??"",t.item.arguments??"");else{if(t.type==="response.incomplete"&&t.response?.incomplete_details?.reason==="max_output_tokens")throw P();if(t.type==="response.failed"||t.type==="error")throw new Error(t.response?.error?.message??t.message??"The model response failed.")}}}async function*k(e,n,t,s,i,o){let c=m=>fetch(`${e}/ai-gateway/mlflow/v1/${n}`,{method:"POST",headers:{...i,Authorization:`Bearer ${m}`,"Content-Type":"application/json"},body:JSON.stringify(t),signal:o}),d=await s.get(),a=await c(d);if(a.status===401&&(await a.body?.cancel(),d=await s.renew(d),a=await c(d)),a.status===401)throw await a.body?.cancel(),new Error(`Databricks rejected the sign-in. Run \`databricks auth login --host ${e}\`, then retry.`);if(!a.ok||!a.body)throw new Error(`AI Gateway returned ${a.status}: ${(await a.text()).slice(0,300)}`);let g=new TextDecoder,p="";for await(let m of a.body){p+=g.decode(m,{stream:!0});let x=p.split(` +`);p=x.pop()??"";for(let J of x){let w=$(J);if(w==="[DONE]")return;w&&(yield JSON.parse(w))}}let u=$(p+g.decode());u&&u!=="[DONE]"&&(yield JSON.parse(u))}var $=e=>e.startsWith("data:")?e.slice(5).trim():"";var N=e=>{let n=e("models");return{workspace:String(e("workspace")??"").replace(/\/+$/,""),models:(Array.isArray(n)?n:[]).filter(t=>typeof t?.id=="string"&&t.id!==""&&(t.api==="chat_completions"||t.api==="responses")),authCommand:String(e("authCommand")??"")}},q=e=>`ucode/${b} githubcopilotchat${typeof e=="string"?`/${e}`:""}`,v=class{constructor(n,t,s=()=>({})){this.settings=n;this.log=t;this.headers=s}settings;log;headers;changed=new l.EventEmitter;onDidChangeLanguageModelChatInformation=this.changed.event;tokens;refresh(){this.tokens=void 0,this.changed.fire()}async provideLanguageModelChatInformation(){let{workspace:n,models:t,authCommand:s}=this.settings();return!n||!s||t.length===0?(this.log("No Databricks models: run `ug configure --agents vscode` to set them up."),[]):t.map(({id:i,api:o})=>({id:i,api:o,name:i,family:i.split(".").pop()??i,version:b,detail:"Databricks",maxInputTokens:128e3,maxOutputTokens:32e3,capabilities:{toolCalling:!0,imageInput:o==="responses"||i.includes("claude")}}))}async provideLanguageModelChatResponse(n,t,s,i,o){let{workspace:c,authCommand:d}=this.settings();this.tokens??=new h(d,u=>this.log(`Fetched a Databricks token (expires ${new Date(u).toISOString()})`));let a={model:n.id,messages:t,tools:s.tools??[],toolRequired:s.toolMode===l.LanguageModelChatToolMode.Required},g=new AbortController,p=o.onCancellationRequested(()=>g.abort());try{let u=n.api==="responses"?G(k(c,"responses",I(a),this.tokens,this.headers(),g.signal)):O(k(c,"chat/completions",D(a),this.tokens,this.headers(),g.signal));for await(let m of u)i.report(m)}catch(u){if(!o.isCancellationRequested)throw this.log(`${n.id} failed: ${u instanceof Error?u.message:String(u)}`),u}finally{p.dispose()}}async provideTokenCount(n,t){return E(t)}};function ne(e){let n=l.window.createOutputChannel("Unity Gateway",{log:!0}),t=()=>{let o=l.workspace.getConfiguration("unityGateway");return N(c=>o.get(c))},s=()=>({"User-Agent":q(l.extensions.getExtension("GitHub.copilot-chat")?.packageJSON?.version)}),i=new v(t,o=>n.info(o),s);e.subscriptions.push(n,l.lm.registerLanguageModelChatProvider("databricks-unity-gateway",i),l.commands.registerCommand("unityGateway.refreshModels",()=>i.refresh()),l.workspace.onDidChangeConfiguration(o=>{o.affectsConfiguration("unityGateway")&&i.refresh()}))}0&&(module.exports={DatabricksModels,activate,readSettings,userAgent}); diff --git a/src/ucode/vscode/extension/package.json b/src/ucode/vscode/extension/package.json index e3d7eea65..624d9aabe 100644 --- a/src/ucode/vscode/extension/package.json +++ b/src/ucode/vscode/extension/package.json @@ -25,7 +25,8 @@ "languageModelChatProviders": [ { "vendor": "databricks-unity-gateway", - "displayName": "Databricks" + "displayName": "Databricks", + "icon": "media/databricks.svg" } ], "configuration": { @@ -81,7 +82,7 @@ ] }, "scripts": { - "build": "esbuild src/extension.ts --bundle --minify --legal-comments=none --outfile=../src/ucode/vscode/extension/out/extension.js --external:vscode --format=cjs --platform=node --target=node22 && node -e \"require('fs').copyFileSync('package.json', '../src/ucode/vscode/extension/package.json')\"", + "build": "esbuild src/extension.ts --bundle --minify --legal-comments=none --outfile=../src/ucode/vscode/extension/out/extension.js --external:vscode --format=cjs --platform=node --target=node22 && node -e \"const fs = require('fs'); fs.copyFileSync('package.json', '../src/ucode/vscode/extension/package.json'); fs.cpSync('media', '../src/ucode/vscode/extension/media', { recursive: true })\"", "typecheck": "tsc", "test": "esbuild test/*.test.ts --bundle --platform=node --format=cjs --target=node22 --outdir=.test-out --alias:vscode=./test/vscode.ts --log-level=warning && node --test .test-out/*.test.js" }, diff --git a/vscode-extension/media/databricks.svg b/vscode-extension/media/databricks.svg new file mode 100644 index 000000000..b7bd91e12 --- /dev/null +++ b/vscode-extension/media/databricks.svg @@ -0,0 +1 @@ + diff --git a/vscode-extension/package.json b/vscode-extension/package.json index e3d7eea65..624d9aabe 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -25,7 +25,8 @@ "languageModelChatProviders": [ { "vendor": "databricks-unity-gateway", - "displayName": "Databricks" + "displayName": "Databricks", + "icon": "media/databricks.svg" } ], "configuration": { @@ -81,7 +82,7 @@ ] }, "scripts": { - "build": "esbuild src/extension.ts --bundle --minify --legal-comments=none --outfile=../src/ucode/vscode/extension/out/extension.js --external:vscode --format=cjs --platform=node --target=node22 && node -e \"require('fs').copyFileSync('package.json', '../src/ucode/vscode/extension/package.json')\"", + "build": "esbuild src/extension.ts --bundle --minify --legal-comments=none --outfile=../src/ucode/vscode/extension/out/extension.js --external:vscode --format=cjs --platform=node --target=node22 && node -e \"const fs = require('fs'); fs.copyFileSync('package.json', '../src/ucode/vscode/extension/package.json'); fs.cpSync('media', '../src/ucode/vscode/extension/media', { recursive: true })\"", "typecheck": "tsc", "test": "esbuild test/*.test.ts --bundle --platform=node --format=cjs --target=node22 --outdir=.test-out --alias:vscode=./test/vscode.ts --log-level=warning && node --test .test-out/*.test.js" }, diff --git a/vscode-extension/test/extension.test.ts b/vscode-extension/test/extension.test.ts index ce1e8f9c4..649eedede 100644 --- a/vscode-extension/test/extension.test.ts +++ b/vscode-extension/test/extension.test.ts @@ -1,4 +1,5 @@ import assert from "node:assert/strict"; +import { existsSync } from "node:fs"; import { afterEach, describe, it } from "node:test"; import type * as vscode from "vscode"; import packageJson from "../package.json"; @@ -74,6 +75,12 @@ describe("settings", () => { }); }); +describe("provider", () => { + it("ships the icon it names", () => { + assert.ok(packageJson.contributes.languageModelChatProviders.every((provider) => existsSync(provider.icon))); + }); +}); + describe("userAgent", () => { it("names ug's extension and Copilot Chat's version", () => { assert.equal(userAgent("0.71.0"), `ucode/${packageJson.version} githubcopilotchat/0.71.0`); From 238af81c0cb95b41cbafb910d00da82005f181c5 Mon Sep 17 00:00:00 2001 From: Xi Lyu Date: Wed, 28 Oct 2026 14:19:20 +0000 Subject: [PATCH 6/6] Take each model's token limits from its settings Every model reported 128K input and 32K output tokens, so VS Code showed a 160K context window for all of them, and Copilot Chat compacted a Claude conversation at 128K although these models take far more. A model entry in unityGateway.models can now carry maxInputTokens and maxOutputTokens. Models without them keep 128K / 32K. Co-authored-by: Isaac --- src/ucode/vscode/extension/out/extension.js | 4 +-- src/ucode/vscode/extension/package.json | 10 ++++++++ vscode-extension/package.json | 10 ++++++++ vscode-extension/src/extension.ts | 28 +++++++++++++++------ vscode-extension/test/extension.test.ts | 22 ++++++++++------ 5 files changed, 57 insertions(+), 17 deletions(-) diff --git a/src/ucode/vscode/extension/out/extension.js b/src/ucode/vscode/extension/out/extension.js index 6a51bc56a..575c17279 100644 --- a/src/ucode/vscode/extension/out/extension.js +++ b/src/ucode/vscode/extension/out/extension.js @@ -1,2 +1,2 @@ -"use strict";var U=Object.create;var f=Object.defineProperty;var F=Object.getOwnPropertyDescriptor;var B=Object.getOwnPropertyNames;var W=Object.getPrototypeOf,H=Object.prototype.hasOwnProperty;var K=(e,n)=>{for(var t in n)f(e,t,{get:n[t],enumerable:!0})},C=(e,n,t,s)=>{if(n&&typeof n=="object"||typeof n=="function")for(let i of B(n))!H.call(e,i)&&i!==t&&f(e,i,{get:()=>n[i],enumerable:!(s=F(n,i))||s.enumerable});return e};var M=(e,n,t)=>(t=e!=null?U(W(e)):{},C(n||!e||!e.__esModule?f(t,"default",{value:e,enumerable:!0}):t,e)),Y=e=>C(f({},"__esModule",{value:!0}),e);var oe={};K(oe,{DatabricksModels:()=>v,activate:()=>ne,readSettings:()=>N,userAgent:()=>q});module.exports=Y(oe);var l=M(require("vscode"));var b="0.1.0";var T=require("node:child_process"),X=5*6e4,Q=60*6e4,V=e=>new Promise((n,t)=>{(0,T.exec)(e,{timeout:6e4,windowsHide:!0},(s,i,o)=>{s?t(new Error(`Couldn't get a Databricks token: \`${e}\` failed: ${String(o).trim().slice(0,500)||s.message}`)):n(String(i).trim())})}),Z=e=>{try{let{exp:n}=JSON.parse(Buffer.from(e.split(".")[1]??"","base64url").toString());if(typeof n=="number")return n*1e3}catch{}return Date.now()+Q},h=class{constructor(n,t=()=>{}){this.command=n;this.onFetch=t}command;onFetch;cached;pending;get(){return this.cached&&this.cached.expiresAt-X>Date.now()?Promise.resolve(this.cached.value):this.fetch(this.command)}async renew(n){return await this.pending?.catch(()=>{}),this.cached&&this.cached.value!==n?this.cached.value:(this.cached=void 0,this.fetch(`${this.command} --force-refresh`))}fetch(n){return this.pending??=V(n).then(t=>{if(!t)throw new Error(`Couldn't get a Databricks token: \`${n}\` printed nothing.`);return this.cached={value:t,expiresAt:Z(t)},this.onFetch(this.cached.expiresAt),t}).finally(()=>{this.pending=void 0}),this.pending}};var r=M(require("vscode")),S=3,ee=1e3,y=e=>e instanceof r.LanguageModelTextPart?e.value:e instanceof r.LanguageModelDataPart&&!e.mimeType.startsWith("image/")?new TextDecoder().decode(e.data):e instanceof r.LanguageModelPromptTsxPart?JSON.stringify(e.value):"",_=e=>e instanceof r.LanguageModelDataPart&&e.mimeType.startsWith("image/")?`data:${e.mimeType};base64,${Buffer.from(e.data).toString("base64")}`:void 0,R=e=>e.inputSchema??{type:"object",properties:{}},L=(e,n,t)=>{let s;try{s=t.trim()?JSON.parse(t):{}}catch{s=void 0}if(typeof s!="object"||s===null||Array.isArray(s))throw new Error(`The model called ${n} with arguments that aren't a JSON object: ${t}`);return new r.LanguageModelToolCallPart(e,n,s)},P=()=>new Error("The model stopped at its output token limit before finishing the response."),A=(e,n)=>e.tools.length?{tools:e.tools.map(n),...e.toolRequired?{tool_choice:"required"}:{}}:{},E=e=>{if(typeof e=="string")return Math.ceil(e.length/4);let n=0,t=0;for(let s of e.content){let i=s instanceof r.LanguageModelToolResultPart?s.content:[s];for(let o of i)n+=o instanceof r.LanguageModelToolCallPart?o.name.length+JSON.stringify(o.input).length:y(o).length,t+=_(o)===void 0?0:1}return Math.ceil(n/4)+t*ee},j=e=>({results:e.filter(n=>n instanceof r.LanguageModelToolResultPart),calls:e.filter(n=>n instanceof r.LanguageModelToolCallPart),text:e.map(y).join(""),images:e.map(_).filter(n=>n!==void 0)});function D(e){let n=e.messages.flatMap(t=>{let{results:s,calls:i,text:o,images:c}=j(t.content);if(Number(t.role)===S)return[{role:"system",content:o}];if(t.role===r.LanguageModelChatMessageRole.Assistant){let a=i.map(g=>({id:g.callId,type:"function",function:{name:g.name,arguments:JSON.stringify(g.input)}}));return o||a.length?[{role:"assistant",content:o||null,...a.length?{tool_calls:a}:{}}]:[]}let d=[...o?[{type:"text",text:o}]:[],...c.map(a=>({type:"image_url",image_url:{url:a}}))];return[...s.map(a=>({role:"tool",tool_call_id:a.callId,content:a.content.map(y).join("")})),...d.length?[{role:"user",content:d}]:[]]});return{model:e.model,messages:n,stream:!0,...A(e,t=>({type:"function",function:{name:t.name,description:t.description,parameters:R(t)}}))}}var te=e=>typeof e=="string"?e:(e??[]).filter(n=>n.type==="text").map(n=>n.text??"").join("");async function*O(e){let n=[];for await(let t of e)for(let s of t.choices??[]){let i=te(s.delta?.content);i&&(yield new r.LanguageModelTextPart(i));for(let o of s.delta?.tool_calls??[]){let c=n[o.index??0]??={id:"",name:"",args:""};c.id=o.id||c.id,c.name=o.function?.name||c.name,c.args+=o.function?.arguments??""}if(s.finish_reason==="length")throw P()}for(let t of n.filter(Boolean))yield L(t.id,t.name,t.args)}function I(e){let n=e.messages.flatMap(t=>{let{results:s,calls:i,text:o,images:c}=j(t.content);if(Number(t.role)===S)return[{role:"system",content:o}];if(t.role===r.LanguageModelChatMessageRole.Assistant)return[...o?[{role:"assistant",content:o}]:[],...i.map(a=>({type:"function_call",call_id:a.callId,name:a.name,arguments:JSON.stringify(a.input)}))];let d=[...o?[{type:"input_text",text:o}]:[],...c.map(a=>({type:"input_image",image_url:a,detail:"auto"}))];return[...s.map(a=>({type:"function_call_output",call_id:a.callId,output:a.content.map(y).join("")})),...d.length?[{role:"user",content:d}]:[]]});return{model:e.model,input:n,stream:!0,store:!1,...A(e,t=>({type:"function",name:t.name,description:t.description,parameters:R(t),strict:!1}))}}async function*G(e){for await(let n of e){let t=n;if(t.type==="response.output_text.delta"&&t.delta)yield new r.LanguageModelTextPart(t.delta);else if(t.type==="response.output_item.done"&&t.item?.type==="function_call")yield L(t.item.call_id??"",t.item.name??"",t.item.arguments??"");else{if(t.type==="response.incomplete"&&t.response?.incomplete_details?.reason==="max_output_tokens")throw P();if(t.type==="response.failed"||t.type==="error")throw new Error(t.response?.error?.message??t.message??"The model response failed.")}}}async function*k(e,n,t,s,i,o){let c=m=>fetch(`${e}/ai-gateway/mlflow/v1/${n}`,{method:"POST",headers:{...i,Authorization:`Bearer ${m}`,"Content-Type":"application/json"},body:JSON.stringify(t),signal:o}),d=await s.get(),a=await c(d);if(a.status===401&&(await a.body?.cancel(),d=await s.renew(d),a=await c(d)),a.status===401)throw await a.body?.cancel(),new Error(`Databricks rejected the sign-in. Run \`databricks auth login --host ${e}\`, then retry.`);if(!a.ok||!a.body)throw new Error(`AI Gateway returned ${a.status}: ${(await a.text()).slice(0,300)}`);let g=new TextDecoder,p="";for await(let m of a.body){p+=g.decode(m,{stream:!0});let x=p.split(` -`);p=x.pop()??"";for(let J of x){let w=$(J);if(w==="[DONE]")return;w&&(yield JSON.parse(w))}}let u=$(p+g.decode());u&&u!=="[DONE]"&&(yield JSON.parse(u))}var $=e=>e.startsWith("data:")?e.slice(5).trim():"";var N=e=>{let n=e("models");return{workspace:String(e("workspace")??"").replace(/\/+$/,""),models:(Array.isArray(n)?n:[]).filter(t=>typeof t?.id=="string"&&t.id!==""&&(t.api==="chat_completions"||t.api==="responses")),authCommand:String(e("authCommand")??"")}},q=e=>`ucode/${b} githubcopilotchat${typeof e=="string"?`/${e}`:""}`,v=class{constructor(n,t,s=()=>({})){this.settings=n;this.log=t;this.headers=s}settings;log;headers;changed=new l.EventEmitter;onDidChangeLanguageModelChatInformation=this.changed.event;tokens;refresh(){this.tokens=void 0,this.changed.fire()}async provideLanguageModelChatInformation(){let{workspace:n,models:t,authCommand:s}=this.settings();return!n||!s||t.length===0?(this.log("No Databricks models: run `ug configure --agents vscode` to set them up."),[]):t.map(({id:i,api:o})=>({id:i,api:o,name:i,family:i.split(".").pop()??i,version:b,detail:"Databricks",maxInputTokens:128e3,maxOutputTokens:32e3,capabilities:{toolCalling:!0,imageInput:o==="responses"||i.includes("claude")}}))}async provideLanguageModelChatResponse(n,t,s,i,o){let{workspace:c,authCommand:d}=this.settings();this.tokens??=new h(d,u=>this.log(`Fetched a Databricks token (expires ${new Date(u).toISOString()})`));let a={model:n.id,messages:t,tools:s.tools??[],toolRequired:s.toolMode===l.LanguageModelChatToolMode.Required},g=new AbortController,p=o.onCancellationRequested(()=>g.abort());try{let u=n.api==="responses"?G(k(c,"responses",I(a),this.tokens,this.headers(),g.signal)):O(k(c,"chat/completions",D(a),this.tokens,this.headers(),g.signal));for await(let m of u)i.report(m)}catch(u){if(!o.isCancellationRequested)throw this.log(`${n.id} failed: ${u instanceof Error?u.message:String(u)}`),u}finally{p.dispose()}}async provideTokenCount(n,t){return E(t)}};function ne(e){let n=l.window.createOutputChannel("Unity Gateway",{log:!0}),t=()=>{let o=l.workspace.getConfiguration("unityGateway");return N(c=>o.get(c))},s=()=>({"User-Agent":q(l.extensions.getExtension("GitHub.copilot-chat")?.packageJSON?.version)}),i=new v(t,o=>n.info(o),s);e.subscriptions.push(n,l.lm.registerLanguageModelChatProvider("databricks-unity-gateway",i),l.commands.registerCommand("unityGateway.refreshModels",()=>i.refresh()),l.workspace.onDidChangeConfiguration(o=>{o.affectsConfiguration("unityGateway")&&i.refresh()}))}0&&(module.exports={DatabricksModels,activate,readSettings,userAgent}); +"use strict";var U=Object.create;var f=Object.defineProperty;var F=Object.getOwnPropertyDescriptor;var B=Object.getOwnPropertyNames;var W=Object.getPrototypeOf,H=Object.prototype.hasOwnProperty;var K=(t,n)=>{for(var e in n)f(t,e,{get:n[e],enumerable:!0})},M=(t,n,e,s)=>{if(n&&typeof n=="object"||typeof n=="function")for(let i of B(n))!H.call(t,i)&&i!==e&&f(t,i,{get:()=>n[i],enumerable:!(s=F(n,i))||s.enumerable});return t};var C=(t,n,e)=>(e=t!=null?U(W(t)):{},M(n||!t||!t.__esModule?f(e,"default",{value:t,enumerable:!0}):e,t)),Y=t=>M(f({},"__esModule",{value:!0}),t);var se={};K(se,{DatabricksModels:()=>v,activate:()=>oe,readSettings:()=>$,userAgent:()=>q});module.exports=Y(se);var l=C(require("vscode"));var b="0.1.0";var T=require("node:child_process"),X=5*6e4,Q=60*6e4,V=t=>new Promise((n,e)=>{(0,T.exec)(t,{timeout:6e4,windowsHide:!0},(s,i,o)=>{s?e(new Error(`Couldn't get a Databricks token: \`${t}\` failed: ${String(o).trim().slice(0,500)||s.message}`)):n(String(i).trim())})}),Z=t=>{try{let{exp:n}=JSON.parse(Buffer.from(t.split(".")[1]??"","base64url").toString());if(typeof n=="number")return n*1e3}catch{}return Date.now()+Q},h=class{constructor(n,e=()=>{}){this.command=n;this.onFetch=e}command;onFetch;cached;pending;get(){return this.cached&&this.cached.expiresAt-X>Date.now()?Promise.resolve(this.cached.value):this.fetch(this.command)}async renew(n){return await this.pending?.catch(()=>{}),this.cached&&this.cached.value!==n?this.cached.value:(this.cached=void 0,this.fetch(`${this.command} --force-refresh`))}fetch(n){return this.pending??=V(n).then(e=>{if(!e)throw new Error(`Couldn't get a Databricks token: \`${n}\` printed nothing.`);return this.cached={value:e,expiresAt:Z(e)},this.onFetch(this.cached.expiresAt),e}).finally(()=>{this.pending=void 0}),this.pending}};var c=C(require("vscode")),S=3,ee=1e3,y=t=>t instanceof c.LanguageModelTextPart?t.value:t instanceof c.LanguageModelDataPart&&!t.mimeType.startsWith("image/")?new TextDecoder().decode(t.data):t instanceof c.LanguageModelPromptTsxPart?JSON.stringify(t.value):"",_=t=>t instanceof c.LanguageModelDataPart&&t.mimeType.startsWith("image/")?`data:${t.mimeType};base64,${Buffer.from(t.data).toString("base64")}`:void 0,R=t=>t.inputSchema??{type:"object",properties:{}},L=(t,n,e)=>{let s;try{s=e.trim()?JSON.parse(e):{}}catch{s=void 0}if(typeof s!="object"||s===null||Array.isArray(s))throw new Error(`The model called ${n} with arguments that aren't a JSON object: ${e}`);return new c.LanguageModelToolCallPart(t,n,s)},P=()=>new Error("The model stopped at its output token limit before finishing the response."),A=(t,n)=>t.tools.length?{tools:t.tools.map(n),...t.toolRequired?{tool_choice:"required"}:{}}:{},E=t=>{if(typeof t=="string")return Math.ceil(t.length/4);let n=0,e=0;for(let s of t.content){let i=s instanceof c.LanguageModelToolResultPart?s.content:[s];for(let o of i)n+=o instanceof c.LanguageModelToolCallPart?o.name.length+JSON.stringify(o.input).length:y(o).length,e+=_(o)===void 0?0:1}return Math.ceil(n/4)+e*ee},O=t=>({results:t.filter(n=>n instanceof c.LanguageModelToolResultPart),calls:t.filter(n=>n instanceof c.LanguageModelToolCallPart),text:t.map(y).join(""),images:t.map(_).filter(n=>n!==void 0)});function I(t){let n=t.messages.flatMap(e=>{let{results:s,calls:i,text:o,images:r}=O(e.content);if(Number(e.role)===S)return[{role:"system",content:o}];if(e.role===c.LanguageModelChatMessageRole.Assistant){let a=i.map(p=>({id:p.callId,type:"function",function:{name:p.name,arguments:JSON.stringify(p.input)}}));return o||a.length?[{role:"assistant",content:o||null,...a.length?{tool_calls:a}:{}}]:[]}let d=[...o?[{type:"text",text:o}]:[],...r.map(a=>({type:"image_url",image_url:{url:a}}))];return[...s.map(a=>({role:"tool",tool_call_id:a.callId,content:a.content.map(y).join("")})),...d.length?[{role:"user",content:d}]:[]]});return{model:t.model,messages:n,stream:!0,...A(t,e=>({type:"function",function:{name:e.name,description:e.description,parameters:R(e)}}))}}var te=t=>typeof t=="string"?t:(t??[]).filter(n=>n.type==="text").map(n=>n.text??"").join("");async function*j(t){let n=[];for await(let e of t)for(let s of e.choices??[]){let i=te(s.delta?.content);i&&(yield new c.LanguageModelTextPart(i));for(let o of s.delta?.tool_calls??[]){let r=n[o.index??0]??={id:"",name:"",args:""};r.id=o.id||r.id,r.name=o.function?.name||r.name,r.args+=o.function?.arguments??""}if(s.finish_reason==="length")throw P()}for(let e of n.filter(Boolean))yield L(e.id,e.name,e.args)}function D(t){let n=t.messages.flatMap(e=>{let{results:s,calls:i,text:o,images:r}=O(e.content);if(Number(e.role)===S)return[{role:"system",content:o}];if(e.role===c.LanguageModelChatMessageRole.Assistant)return[...o?[{role:"assistant",content:o}]:[],...i.map(a=>({type:"function_call",call_id:a.callId,name:a.name,arguments:JSON.stringify(a.input)}))];let d=[...o?[{type:"input_text",text:o}]:[],...r.map(a=>({type:"input_image",image_url:a,detail:"auto"}))];return[...s.map(a=>({type:"function_call_output",call_id:a.callId,output:a.content.map(y).join("")})),...d.length?[{role:"user",content:d}]:[]]});return{model:t.model,input:n,stream:!0,store:!1,...A(t,e=>({type:"function",name:e.name,description:e.description,parameters:R(e),strict:!1}))}}async function*G(t){for await(let n of t){let e=n;if(e.type==="response.output_text.delta"&&e.delta)yield new c.LanguageModelTextPart(e.delta);else if(e.type==="response.output_item.done"&&e.item?.type==="function_call")yield L(e.item.call_id??"",e.item.name??"",e.item.arguments??"");else{if(e.type==="response.incomplete"&&e.response?.incomplete_details?.reason==="max_output_tokens")throw P();if(e.type==="response.failed"||e.type==="error")throw new Error(e.response?.error?.message??e.message??"The model response failed.")}}}async function*k(t,n,e,s,i,o){let r=m=>fetch(`${t}/ai-gateway/mlflow/v1/${n}`,{method:"POST",headers:{...i,Authorization:`Bearer ${m}`,"Content-Type":"application/json"},body:JSON.stringify(e),signal:o}),d=await s.get(),a=await r(d);if(a.status===401&&(await a.body?.cancel(),d=await s.renew(d),a=await r(d)),a.status===401)throw await a.body?.cancel(),new Error(`Databricks rejected the sign-in. Run \`databricks auth login --host ${t}\`, then retry.`);if(!a.ok||!a.body)throw new Error(`AI Gateway returned ${a.status}: ${(await a.text()).slice(0,300)}`);let p=new TextDecoder,g="";for await(let m of a.body){g+=p.decode(m,{stream:!0});let x=g.split(` +`);g=x.pop()??"";for(let J of x){let w=N(J);if(w==="[DONE]")return;w&&(yield JSON.parse(w))}}let u=N(g+p.decode());u&&u!=="[DONE]"&&(yield JSON.parse(u))}var N=t=>t.startsWith("data:")?t.slice(5).trim():"";var ne=t=>Object.fromEntries(["maxInputTokens","maxOutputTokens"].flatMap(n=>{let e=t[n];return typeof e=="number"&&Number.isInteger(e)&&e>0?[[n,e]]:[]})),$=t=>{let n=t("models");return{workspace:String(t("workspace")??"").replace(/\/+$/,""),models:(Array.isArray(n)?n:[]).filter(e=>typeof e?.id=="string"&&e.id!==""&&(e.api==="chat_completions"||e.api==="responses")).map(e=>({id:e.id,api:e.api,...ne(e)})),authCommand:String(t("authCommand")??"")}},q=t=>`ucode/${b} githubcopilotchat${typeof t=="string"?`/${t}`:""}`,v=class{constructor(n,e,s=()=>({})){this.settings=n;this.log=e;this.headers=s}settings;log;headers;changed=new l.EventEmitter;onDidChangeLanguageModelChatInformation=this.changed.event;tokens;refresh(){this.tokens=void 0,this.changed.fire()}async provideLanguageModelChatInformation(){let{workspace:n,models:e,authCommand:s}=this.settings();return!n||!s||e.length===0?(this.log("No Databricks models: run `ug configure --agents vscode` to set them up."),[]):e.map(({id:i,api:o,maxInputTokens:r,maxOutputTokens:d})=>({id:i,api:o,name:i,family:i.split(".").pop()??i,version:b,detail:"Databricks",maxInputTokens:r??128e3,maxOutputTokens:d??32e3,capabilities:{toolCalling:!0,imageInput:o==="responses"||i.includes("claude")}}))}async provideLanguageModelChatResponse(n,e,s,i,o){let{workspace:r,authCommand:d}=this.settings();this.tokens??=new h(d,u=>this.log(`Fetched a Databricks token (expires ${new Date(u).toISOString()})`));let a={model:n.id,messages:e,tools:s.tools??[],toolRequired:s.toolMode===l.LanguageModelChatToolMode.Required},p=new AbortController,g=o.onCancellationRequested(()=>p.abort());try{let u=n.api==="responses"?G(k(r,"responses",D(a),this.tokens,this.headers(),p.signal)):j(k(r,"chat/completions",I(a),this.tokens,this.headers(),p.signal));for await(let m of u)i.report(m)}catch(u){if(!o.isCancellationRequested)throw this.log(`${n.id} failed: ${u instanceof Error?u.message:String(u)}`),u}finally{g.dispose()}}async provideTokenCount(n,e){return E(e)}};function oe(t){let n=l.window.createOutputChannel("Unity Gateway",{log:!0}),e=()=>{let o=l.workspace.getConfiguration("unityGateway");return $(r=>o.get(r))},s=()=>({"User-Agent":q(l.extensions.getExtension("GitHub.copilot-chat")?.packageJSON?.version)}),i=new v(e,o=>n.info(o),s);t.subscriptions.push(n,l.lm.registerLanguageModelChatProvider("databricks-unity-gateway",i),l.commands.registerCommand("unityGateway.refreshModels",()=>i.refresh()),l.workspace.onDidChangeConfiguration(o=>{o.affectsConfiguration("unityGateway")&&i.refresh()}))}0&&(module.exports={DatabricksModels,activate,readSettings,userAgent}); diff --git a/src/ucode/vscode/extension/package.json b/src/ucode/vscode/extension/package.json index 624d9aabe..8bc404b46 100644 --- a/src/ucode/vscode/extension/package.json +++ b/src/ucode/vscode/extension/package.json @@ -60,6 +60,16 @@ "responses" ], "description": "The AI Gateway API the model is called through." + }, + "maxInputTokens": { + "type": "integer", + "minimum": 1, + "description": "Prompt tokens the model accepts. Defaults to 128000." + }, + "maxOutputTokens": { + "type": "integer", + "minimum": 1, + "description": "Tokens the model can write in one reply. Defaults to 32000." } } }, diff --git a/vscode-extension/package.json b/vscode-extension/package.json index 624d9aabe..8bc404b46 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -60,6 +60,16 @@ "responses" ], "description": "The AI Gateway API the model is called through." + }, + "maxInputTokens": { + "type": "integer", + "minimum": 1, + "description": "Prompt tokens the model accepts. Defaults to 128000." + }, + "maxOutputTokens": { + "type": "integer", + "minimum": 1, + "description": "Tokens the model can write in one reply. Defaults to 32000." } } }, diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index 8d1bf42df..991e27485 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -7,6 +7,8 @@ import { streamEvents } from "./gateway"; export interface ModelSetting { readonly id: string; readonly api: "chat_completions" | "responses"; + readonly maxInputTokens?: number; + readonly maxOutputTokens?: number; } export interface Settings { @@ -15,14 +17,24 @@ export interface Settings { readonly authCommand: string; } +const tokenLimits = (model: ModelSetting): Partial => + Object.fromEntries( + (["maxInputTokens", "maxOutputTokens"] as const).flatMap((key) => { + const value: unknown = model[key]; + return typeof value === "number" && Number.isInteger(value) && value > 0 ? [[key, value]] : []; + }), + ); + export const readSettings = (get: (key: string) => unknown): Settings => { const models = get("models"); return { workspace: String(get("workspace") ?? "").replace(/\/+$/, ""), - models: (Array.isArray(models) ? models : []).filter( - (model): model is ModelSetting => - typeof model?.id === "string" && model.id !== "" && (model.api === "chat_completions" || model.api === "responses"), - ), + models: (Array.isArray(models) ? models : []) + .filter( + (model): model is ModelSetting => + typeof model?.id === "string" && model.id !== "" && (model.api === "chat_completions" || model.api === "responses"), + ) + .map((model) => ({ id: model.id, api: model.api, ...tokenLimits(model) })), authCommand: String(get("authCommand") ?? ""), }; }; @@ -56,7 +68,7 @@ export class DatabricksModels implements vscode.LanguageModelChatProvider this.log("No Databricks models: run `ug configure --agents vscode` to set them up."); return []; } - return models.map(({ id, api }) => ({ + return models.map(({ id, api, maxInputTokens, maxOutputTokens }) => ({ id, api, name: id, @@ -64,9 +76,9 @@ export class DatabricksModels implements vscode.LanguageModelChatProvider family: id.split(".").pop() ?? id, version, detail: "Databricks", - // The gateway reports no limits for these models. - maxInputTokens: 128_000, - maxOutputTokens: 32_000, + // The gateway reports no limits, so a model ug configure has none for gets Copilot's 128K default. + maxInputTokens: maxInputTokens ?? 128_000, + maxOutputTokens: maxOutputTokens ?? 32_000, capabilities: { toolCalling: true, imageInput: api === "responses" || id.includes("claude") }, })); } diff --git a/vscode-extension/test/extension.test.ts b/vscode-extension/test/extension.test.ts index 649eedede..5e94cfcf4 100644 --- a/vscode-extension/test/extension.test.ts +++ b/vscode-extension/test/extension.test.ts @@ -89,18 +89,26 @@ describe("userAgent", () => { }); describe("readSettings", () => { - it("keeps well-formed models and trims the workspace URL", () => { + it("keeps well-formed models and token limits and trims the workspace URL", () => { const values: Record = { workspace: "https://ws/", authCommand: "ug auth-token", - models: [{ id: "a", api: "responses" }, { id: "b", api: "messages" }, { api: "responses" }, null, { id: "c", api: "chat_completions" }], + models: [ + { id: "a", api: "responses" }, + { id: "b", api: "messages" }, + { api: "responses" }, + null, + { id: "c", api: "chat_completions", maxInputTokens: 936_000, maxOutputTokens: "32000", extra: true }, + { id: "d", api: "chat_completions", maxInputTokens: 0, maxOutputTokens: 1.5 }, + ], }; assert.deepEqual(readSettings((key) => values[key]), { workspace: "https://ws", authCommand: "ug auth-token", models: [ { id: "a", api: "responses" }, - { id: "c", api: "chat_completions" }, + { id: "c", api: "chat_completions", maxInputTokens: 936_000 }, + { id: "d", api: "chat_completions" }, ], }); }); @@ -111,7 +119,7 @@ describe("DatabricksModels", () => { workspace: "https://ws", authCommand: command, models: [ - { id: "system.ai.claude-sonnet-5-5", api: "chat_completions" }, + { id: "system.ai.claude-sonnet-5-5", api: "chat_completions", maxInputTokens: 872_000, maxOutputTokens: 128_000 }, { id: "system.ai.gpt-6-luna", api: "responses" }, ], }); @@ -120,10 +128,10 @@ describe("DatabricksModels", () => { const logs: string[] = []; const models = await new DatabricksModels(() => settings("cmd"), (line) => logs.push(line)).provideLanguageModelChatInformation(); assert.deepEqual( - models.map((model) => [model.id, model.family, model.api]), + models.map((model) => [model.id, model.family, model.api, model.maxInputTokens, model.maxOutputTokens]), [ - ["system.ai.claude-sonnet-5-5", "claude-sonnet-5-5", "chat_completions"], - ["system.ai.gpt-6-luna", "gpt-6-luna", "responses"], + ["system.ai.claude-sonnet-5-5", "claude-sonnet-5-5", "chat_completions", 872_000, 128_000], + ["system.ai.gpt-6-luna", "gpt-6-luna", "responses", 128_000, 32_000], ], ); assert.deepEqual(await new DatabricksModels(() => ({ ...settings(""), models: [] }), (line) => logs.push(line)).provideLanguageModelChatInformation(), []);