Compare commits

..
38 changed files with 356 additions and 523 deletions
+11 -19
View File
@@ -301,7 +301,7 @@
"@ai-sdk/deepinfra": "2.0.41",
"@ai-sdk/gateway": "3.0.104",
"@ai-sdk/google": "3.0.73",
"@ai-sdk/google-vertex": "4.0.128",
"@ai-sdk/google-vertex": "4.0.181",
"@ai-sdk/groq": "3.0.31",
"@ai-sdk/mistral": "3.0.51",
"@ai-sdk/openai": "3.0.84",
@@ -576,7 +576,7 @@
"@ai-sdk/deepinfra": "2.0.41",
"@ai-sdk/gateway": "3.0.104",
"@ai-sdk/google": "3.0.73",
"@ai-sdk/google-vertex": "4.0.128",
"@ai-sdk/google-vertex": "4.0.181",
"@ai-sdk/groq": "3.0.31",
"@ai-sdk/mistral": "3.0.51",
"@ai-sdk/openai": "3.0.84",
@@ -1197,7 +1197,7 @@
"@ai-sdk/google": ["@ai-sdk/google@3.0.73", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-o2MuIeyvZrFIeIbnbA8Thrr63irdyUBh0uWBZ2lY6yFeXuE/tcwyXF74bDKS4KvTu84uFpQfpbS/LXHGKKXz+g=="],
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.128", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.77", "@ai-sdk/google": "3.0.73", "@ai-sdk/openai-compatible": "2.0.47", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jK8fixb4km2yfgvb9DUFQRpV/jiDB0v9gyxHoHfPydaQvz+CpAz8DTt1quyaM+Wg9G2R8Zo68CYmHbIkUqW2AA=="],
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.181", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.110", "@ai-sdk/google": "3.0.108", "@ai-sdk/openai-compatible": "2.0.67", "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-57b5Qor8V53vubkxCj09tbHWpzpCLUbzmll2FwShuLvyEAsCH6mh3sAowDhiwUWPXnLzU+rC3RVMKCPscqICcg=="],
"@ai-sdk/groq": ["@ai-sdk/groq@3.0.31", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XbbugpnFmXGu2TlXiq8KUJskP6/VVbuFcnFIGDzDIB/Chg6XHsNnqrTF80Zxkh0Pd3+NvbM+2Uqrtsndk6bDAg=="],
@@ -5691,13 +5691,15 @@
"@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.77", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ML8C2M1YvPA1ulEx4TiyF0k1xvC2ikEiPBIC1PPQ0a5xELUGrO2lAaEzsTEoJ+eCeDd8PSBuFJjs+r+9yIwQXA=="],
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.110", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rNkamQCeAUOUGr5Npg5pXZyYFH4fS1U6Mbdy3dF/NNBEI3D2Chc/ruRrwNegP0gfpX3cllP3O4jSibGBbWPZ7A=="],
"@ai-sdk/google-vertex/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Enm5UlL0zUCrW3792opk5h7hRWxZOZzDe6eQYVFqX9LUOGGCe1h8MZWAGim765nwzgnjlpeYOsuzZmLtRsTPlg=="],
"@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@3.0.108", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kwvYpRNghqt0VRKE7Hx1UWZQCUJJFqUITj24baxy+ApS0Hru0PkBJHD75a36Wc+e6e+wHcKR2MconTeJiBZigA=="],
"@ai-sdk/google-vertex/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
"@ai-sdk/google-vertex/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-glcEJC2mBXJKj7joFI0fRhcbdDYKTBgXMPcT6Vcnlym67tTzuNG9pFx3zblxVv8TdOxhojJja5zGG19yeGJxuA=="],
"@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
"@ai-sdk/google-vertex/@ai-sdk/provider": ["@ai-sdk/provider@3.0.15", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-XeZW1CcDF2GMbH4wejW6xBRI2QCOgnkVYUnxoeDadB1mf85riL2bMUeDoh+6gJ/r4mjNfzUPW8OjLjvwTP0u1Q=="],
"@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.45", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^5.29.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7u5B/E2uZmU65SlJhhQGFHZwRCN0xOz4HHtFc4sEGV9PHbX3fGiEiZBpc/SABay1dGeJgK3VD60rvLGoWdWPXA=="],
"@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
@@ -6181,8 +6183,6 @@
"ai-gateway-provider/@ai-sdk/google": ["@ai-sdk/google@3.0.108", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kwvYpRNghqt0VRKE7Hx1UWZQCUJJFqUITj24baxy+ApS0Hru0PkBJHD75a36Wc+e6e+wHcKR2MconTeJiBZigA=="],
"ai-gateway-provider/@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.181", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.110", "@ai-sdk/google": "3.0.108", "@ai-sdk/openai-compatible": "2.0.67", "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-57b5Qor8V53vubkxCj09tbHWpzpCLUbzmll2FwShuLvyEAsCH6mh3sAowDhiwUWPXnLzU+rC3RVMKCPscqICcg=="],
"ai-gateway-provider/@ai-sdk/groq": ["@ai-sdk/groq@3.0.59", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-X4h60TGq4pIOXPsthatUr+bfTaYCaKGX597hG9JgcueEl4+nboCdw99ixjFKGkvYlBJwLCCfI957EmGA2QlF0w=="],
"ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="],
@@ -6615,6 +6615,8 @@
"@ai-sdk/google-vertex/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/google-vertex/@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
"@ai-sdk/google/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
@@ -7015,12 +7017,6 @@
"ai-gateway-provider/@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.45", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^5.29.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7u5B/E2uZmU65SlJhhQGFHZwRCN0xOz4HHtFc4sEGV9PHbX3fGiEiZBpc/SABay1dGeJgK3VD60rvLGoWdWPXA=="],
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-glcEJC2mBXJKj7joFI0fRhcbdDYKTBgXMPcT6Vcnlym67tTzuNG9pFx3zblxVv8TdOxhojJja5zGG19yeGJxuA=="],
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider": ["@ai-sdk/provider@3.0.15", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-XeZW1CcDF2GMbH4wejW6xBRI2QCOgnkVYUnxoeDadB1mf85riL2bMUeDoh+6gJ/r4mjNfzUPW8OjLjvwTP0u1Q=="],
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.45", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^5.29.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7u5B/E2uZmU65SlJhhQGFHZwRCN0xOz4HHtFc4sEGV9PHbX3fGiEiZBpc/SABay1dGeJgK3VD60rvLGoWdWPXA=="],
"ai-gateway-provider/@ai-sdk/groq/@ai-sdk/provider": ["@ai-sdk/provider@3.0.15", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-XeZW1CcDF2GMbH4wejW6xBRI2QCOgnkVYUnxoeDadB1mf85riL2bMUeDoh+6gJ/r4mjNfzUPW8OjLjvwTP0u1Q=="],
"ai-gateway-provider/@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.45", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^5.29.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7u5B/E2uZmU65SlJhhQGFHZwRCN0xOz4HHtFc4sEGV9PHbX3fGiEiZBpc/SABay1dGeJgK3VD60rvLGoWdWPXA=="],
@@ -7463,10 +7459,6 @@
"ai-gateway-provider/@ai-sdk/cohere/@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
"ai-gateway-provider/@ai-sdk/google/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"ai-gateway-provider/@ai-sdk/google/@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-kDCnJMnaK/Jq7ckcpPB7Vl9v98EMSdcehZAtf8jNjTs=",
"aarch64-linux": "sha256-0aR+OJGXS5HMlXbe/BHybjIRvdNJJw6gjW+jr6Dk7Pk=",
"aarch64-darwin": "sha256-loLrV6xiorhwS/N2hlpiKSKX172Qxy+auNiPzFBhQSc=",
"x86_64-darwin": "sha256-PNEpQBLAz8M274bSyTpp0jofETn2L+D0uBiJHUV7nB0="
"x86_64-linux": "sha256-yQ8EIxxYkzlEWIMY/UiIR+7lbGBuizsoehMPZzFA65Y=",
"aarch64-linux": "sha256-JF9VVgnl5QUZ430fqb5Qu8y0kchYJ00LO3FbYcd0lBM=",
"aarch64-darwin": "sha256-f3Tu6eu463NWcHgr7dupfD/zUTh26bJ6N2vVXuEyi6c=",
"x86_64-darwin": "sha256-miv9Sv4KdhD0UIi2O5LVS1wQOfq29IV+9S+BvIzAvgo="
}
}
@@ -145,7 +145,7 @@ function ServerForm(props: ServerFormProps) {
type="text"
label={language.t("dialog.server.add.name")}
placeholder={language.t("dialog.server.add.namePlaceholder")}
value={props.name}
defaultValue={props.name}
disabled={props.busy}
onChange={props.onNameChange}
onKeyDown={keyDown}
@@ -155,7 +155,7 @@ function ServerForm(props: ServerFormProps) {
type="text"
label={language.t("dialog.server.add.username")}
placeholder={language.t("dialog.server.add.usernamePlaceholder")}
value={props.username}
defaultValue={props.username}
disabled={props.busy}
onChange={props.onUsernameChange}
onKeyDown={keyDown}
@@ -164,7 +164,7 @@ function ServerForm(props: ServerFormProps) {
type="password"
label={language.t("dialog.server.add.password")}
placeholder={language.t("dialog.server.add.passwordPlaceholder")}
value={props.password}
defaultValue={props.password}
disabled={props.busy}
onChange={props.onPasswordChange}
onKeyDown={keyDown}
+1 -1
View File
@@ -73,10 +73,10 @@ function LimitsGraph(props: { href: string }) {
{ id: "glm-5.2", name: "GLM-5.2", req: 880, d: "100ms" },
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", req: 1050, d: "150ms" },
{ id: "minimax-m3", name: "MiniMax M3", req: 3200, d: "210ms" },
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", req: 3800, d: "270ms" },
{ id: "gpt-5.6-luna", name: "GPT 5.6 Luna", req: 4100, baseReq: 2050, d: "290ms" },
{ id: "qwen3.7-plus", name: "Qwen3.7 Plus", req: 4300, d: "300ms" },
{ id: "hy3", name: "Hy3", req: 4300, d: "320ms" },
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", req: 7600, d: "330ms" },
{ id: "mimo-v2.5", name: "MiMo-V2.5", req: 30100, edge: true, d: "340ms" },
]
@@ -247,7 +247,7 @@ export async function handler(
headers.delete("host")
headers.delete("content-length")
headers.delete("x-opencode-request")
headers.delete("x-opencode-session")
if (!isNewInference) headers.delete("x-opencode-session")
headers.delete("x-opencode-project")
headers.delete("x-opencode-client")
return headers
+1 -1
View File
@@ -70,7 +70,7 @@
"@ai-sdk/deepinfra": "2.0.41",
"@ai-sdk/gateway": "3.0.104",
"@ai-sdk/google": "3.0.73",
"@ai-sdk/google-vertex": "4.0.128",
"@ai-sdk/google-vertex": "4.0.181",
"@ai-sdk/groq": "3.0.31",
"@ai-sdk/mistral": "3.0.51",
"@ai-sdk/openai": "3.0.84",
+1
View File
@@ -202,6 +202,7 @@ export const make = (dependencies: Dependencies) => {
.stream(
LLM.request({
model: input.model,
http: input.request.http,
messages: [Message.user(summaryPrompt)],
tools: [],
generation: { maxTokens: summaryOutput },
+7
View File
@@ -204,6 +204,13 @@ const layer = Layer.effect(
const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(session.id) ? session.id.slice(4) : session.id
const request = LLM.request({
model,
http: {
headers: {
"x-session-affinity": session.id,
"X-Session-Id": session.id,
...(session.parentID ? { "x-parent-session-id": session.parentID } : {}),
},
},
providerOptions: { openai: { promptCacheKey } },
system: [agent.info?.system, system.baseline]
.filter((part): part is string => part !== undefined && part.length > 0)
+47
View File
@@ -1101,6 +1101,16 @@ describe("SessionRunnerLLM", () => {
yield* session.resume(sessionID)
expect(requests).toHaveLength(2)
expect(requests.map((request) => request.http?.headers)).toEqual([
{
"x-session-affinity": sessionID,
"X-Session-Id": sessionID,
},
{
"x-session-affinity": sessionID,
"X-Session-Id": sessionID,
},
])
expect(userTexts(requests[0])[0]).toContain("## Objective")
expect(userTexts(requests[1])).toHaveLength(1)
expect(userTexts(requests[1])[0]).toContain("<summary>\n## Objective\n- Preserve the task\n</summary>")
@@ -2508,6 +2518,43 @@ describe("SessionRunnerLLM", () => {
}),
)
it.effect("adds session correlation headers to model requests", () =>
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Run correlated request" }), resume: false })
requests.length = 0
yield* session.resume(sessionID)
expect(requests[0]?.http?.headers).toEqual({
"x-session-affinity": sessionID,
"X-Session-Id": sessionID,
})
}),
)
it.effect("adds the parent session header to child model requests", () =>
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
const parentID = SessionV2.ID.make("ses_runner_parent")
const { db } = yield* Database.Service
yield* db
.update(SessionTable)
.set({ parent_id: parentID })
.where(eq(SessionTable.id, sessionID))
.run()
.pipe(Effect.orDie)
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Run child request" }), resume: false })
requests.length = 0
yield* session.resume(sessionID)
expect(requests[0]?.http?.headers?.["x-parent-session-id"]).toBe(parentID)
}),
)
it.effect("bounds 64-character session prompt cache keys", () =>
Effect.gen(function* () {
yield* setup
+1 -1
View File
@@ -64,7 +64,7 @@
"@ai-sdk/deepinfra": "2.0.41",
"@ai-sdk/gateway": "3.0.104",
"@ai-sdk/google": "3.0.73",
"@ai-sdk/google-vertex": "4.0.128",
"@ai-sdk/google-vertex": "4.0.181",
"@ai-sdk/groq": "3.0.31",
"@ai-sdk/mistral": "3.0.51",
"@ai-sdk/openai": "3.0.84",
+1 -152
View File
@@ -1,70 +1,6 @@
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
import type { Provider } from "@opencode-ai/sdk/v2"
import { Schema } from "effect"
import { OAUTH_DUMMY_KEY } from "../auth"
const AZURE_COGNITIVE_SERVICES_SCOPE = "https://cognitiveservices.azure.com/.default"
const AZURE_FOUNDRY_SCOPE = "https://ai.azure.com/.default"
const AZURE_TOKEN_REFRESH_BUFFER = 60_000
const AzureCliToken = Schema.Struct({
accessToken: Schema.NonEmptyString,
expires_on: Schema.Number,
})
const decodeAzureCliToken = Schema.decodeUnknownPromise(AzureCliToken)
const decodeAzureAccounts = Schema.decodeUnknownPromise(
Schema.Array(
Schema.Struct({
name: Schema.NonEmptyString,
resourceGroup: Schema.NonEmptyString,
}),
),
)
const decodeAzureDeployments = Schema.decodeUnknownPromise(
Schema.Array(
Schema.Struct({
name: Schema.NonEmptyString,
properties: Schema.Struct({
model: Schema.Struct({
name: Schema.NonEmptyString,
}),
provisioningState: Schema.NonEmptyString,
}),
}),
),
)
type AzureCommand = {
quiet(): AzureCommand
json(): Promise<unknown>
}
type AzureShell = (strings: TemplateStringsArray, ...values: string[]) => AzureCommand
export async function AzureAuthPlugin(input: PluginInput): Promise<Hooks> {
return createAzureAuthHooks(input.$)
}
export function createAzureAuthHooks(
shell: AzureShell,
request: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> = fetch,
): Hooks {
const tokens = new Map<string, { token: string; expires: number }>()
async function token(scope: string) {
const cached = tokens.get(scope)
if (cached && cached.expires - Date.now() > AZURE_TOKEN_REFRESH_BUFFER) return cached.token
const result = await decodeAzureCliToken(
await shell`az account get-access-token --scope ${scope} --output json`.quiet().json(),
)
const token = { token: result.accessToken, expires: result.expires_on * 1000 }
tokens.set(scope, token)
return token.token
}
export async function AzureAuthPlugin(_input: PluginInput): Promise<Hooks> {
const prompts = []
if (!process.env.AZURE_RESOURCE_NAME) {
prompts.push({
@@ -76,102 +12,15 @@ export function createAzureAuthHooks(
}
return {
provider: {
id: "azure",
async models(provider, context) {
if (context.auth?.type !== "oauth") return provider.models
if (!context.auth.accountId) return {}
return discoverAzureModels(provider.models, context.auth.accountId, shell).catch(() => ({}))
},
},
auth: {
provider: "azure",
async loader(getAuth) {
if ((await getAuth()).type !== "oauth") return {}
return {
apiKey: OAUTH_DUMMY_KEY,
async fetch(input: RequestInfo | URL, init?: RequestInit) {
const headers = new Headers(input instanceof Request ? input.headers : undefined)
new Headers(init?.headers).forEach((value, key) => headers.set(key, value))
headers.delete("api-key")
headers.delete("x-api-key")
headers.set("authorization", `Bearer ${await token(scopeForRequest(input))}`)
headers.set("User-Agent", `opencode/${InstallationVersion}`)
return request(input, { ...init, headers })
},
}
},
methods: [
{
type: "api",
label: "API key",
prompts,
},
{
type: "oauth",
label: "Microsoft Entra ID (Azure CLI)",
prompts,
async authorize(inputs) {
return {
url: "",
instructions: "Sign in with `az login` before continuing.",
method: "auto",
callback: async () => {
const resourceName = inputs?.resourceName ?? process.env.AZURE_RESOURCE_NAME
if (!resourceName) throw new Error("Azure Resource Name is required")
await token(AZURE_COGNITIVE_SERVICES_SCOPE)
return {
type: "success",
access: OAUTH_DUMMY_KEY,
refresh: OAUTH_DUMMY_KEY,
expires: Date.now() + 365 * 24 * 60 * 60 * 1000,
accountId: resourceName,
}
},
}
},
},
],
},
}
}
async function discoverAzureModels(models: Provider["models"], resourceName: string, shell: AzureShell) {
const accounts = await decodeAzureAccounts(
await shell`az cognitiveservices account list --output json --only-show-errors`.quiet().json(),
)
const account = accounts.find((account) => account.name.toLowerCase() === resourceName.toLowerCase())
if (!account) return {}
const deployments = await decodeAzureDeployments(
await shell`az cognitiveservices account deployment list --name ${account.name} --resource-group ${account.resourceGroup} --output json --only-show-errors`
.quiet()
.json(),
)
const found = new Map<string, Provider["models"][string]>()
deployments.forEach((deployment) => {
if (deployment.properties.provisioningState !== "Succeeded") return
const modelID = Object.keys(models).find(
(modelID) => modelID.toLowerCase() === deployment.properties.model.name.toLowerCase(),
)
if (!modelID) return
found.set(modelID, {
...models[modelID],
api: {
...models[modelID].api,
id: deployment.name,
},
})
})
return Object.fromEntries(found)
}
function scopeForRequest(input: RequestInfo | URL) {
const url = new URL(input instanceof Request ? input.url : input)
if (url.hostname.endsWith(".services.ai.azure.com") && !url.pathname.startsWith("/models")) {
return AZURE_FOUNDRY_SCOPE
}
return AZURE_COGNITIVE_SERVICES_SCOPE
}
+16 -4
View File
@@ -37,10 +37,12 @@ function base64UrlEncode(buffer: ArrayBuffer): string {
export interface IdTokenClaims {
chatgpt_account_id?: string
chatgpt_compute_residency?: string
organizations?: Array<{ id: string }>
email?: string
"https://api.openai.com/auth"?: {
chatgpt_account_id?: string
chatgpt_compute_residency?: string
}
}
@@ -75,6 +77,14 @@ export function extractAccountId(tokens: TokenResponse): string | undefined {
return undefined
}
export function extractResidency(token: string): string | undefined {
const claims = parseJwtClaims(token)
const residency =
claims?.["https://api.openai.com/auth"]?.chatgpt_compute_residency ?? claims?.chatgpt_compute_residency
if (!residency || residency === "no_constraint") return undefined
return residency
}
function buildAuthorizeUrl(redirectUri: string, pkce: PkceCodes, state: string): string {
const params = new URLSearchParams({
response_type: "code",
@@ -406,10 +416,12 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug
requestInput instanceof URL
? requestInput
: new URL(typeof requestInput === "string" ? requestInput : requestInput.url)
const url =
parsed.pathname.includes("/v1/responses") || parsed.pathname.includes("/chat/completions")
? new URL(codexApiEndpoint)
: parsed
const rewrite = parsed.pathname.includes("/v1/responses") || parsed.pathname.includes("/chat/completions")
const url = rewrite ? new URL(codexApiEndpoint) : parsed
if (rewrite) {
const residency = extractResidency(currentAuth.access)
if (residency) headers.set("x-openai-internal-codex-residency", residency)
}
const requestInit = {
...init,
@@ -110,10 +110,11 @@ export function createWebSocketFetch(options?: CreateWebSocketFetchOptions) {
invalidate(entry)
}
},
onConnectionInvalid: (error) => {
onConnectionInvalid: (_error, closeCode) => {
entry.busy = false
entry.lastUsedAt = Date.now()
if (!entry.fallback) recordStreamFailure(entry)
if (closeCode === OpenAIWebSocket.MESSAGE_TOO_BIG_CLOSE_CODE) entry.fallback = true
else if (!entry.fallback) recordStreamFailure(entry)
invalidate(entry)
resolveFirstEvent(false)
},
+6 -4
View File
@@ -9,6 +9,7 @@ import { ProxyEnv } from "@/util/proxy-env"
import { isRecord } from "@/util/record"
export const PROTOCOL_HEADER = "responses_websockets=2026-02-06"
export const MESSAGE_TOO_BIG_CLOSE_CODE = 1009
export interface ConnectResponsesWebSocketOptions {
url: string
@@ -26,7 +27,7 @@ export interface StreamResponsesWebSocketOptions {
onComplete?: (event: Record<string, unknown>) => void
onTerminal?: (event: Record<string, unknown>) => void
onRetryableTerminal?: (event: Record<string, unknown>) => Promise<WebSocket | undefined>
onConnectionInvalid?: (error: ProviderError.ResponseStreamError) => void
onConnectionInvalid?: (error: ProviderError.ResponseStreamError, closeCode?: number) => void
onAbort?: (error: Error) => void
}
@@ -162,11 +163,11 @@ export function streamResponsesWebSocket(options: StreamResponsesWebSocketOption
controller?.close()
}
function invalidate(error: ProviderError.ResponseStreamError) {
function invalidate(error: ProviderError.ResponseStreamError, closeCode?: number) {
if (completed) return
completed = true
cleanup()
options.onConnectionInvalid?.(error)
options.onConnectionInvalid?.(error, closeCode)
controller?.error(error)
}
@@ -274,6 +275,7 @@ export function streamResponsesWebSocket(options: StreamResponsesWebSocketOption
if (completed) return
invalidate(
new ProviderError.ResponseStreamError(closeMessage("WebSocket closed before response.completed", code, reason)),
code,
)
}
@@ -373,7 +375,7 @@ function abortError(signal: AbortSignal | undefined) {
function closeMessage(message: string, code: number, reason: Buffer) {
const details = [`code ${code}`]
if (code === 1009) details.push("message too big")
if (code === MESSAGE_TOO_BIG_CLOSE_CODE) details.push("message too big")
if (reason.length > 0) details.push(reason.toString())
return `${message} (${details.join(": ")})`
}
@@ -244,7 +244,6 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
return [
provider.options?.resourceName,
auth?.type === "api" ? auth.metadata?.resourceName : undefined,
auth?.type === "oauth" ? auth.accountId : undefined,
env["AZURE_RESOURCE_NAME"],
].find((name) => typeof name === "string" && name.trim() !== "")
})
-259
View File
@@ -1,259 +0,0 @@
import { afterEach, describe, expect, test } from "bun:test"
import type { Hooks } from "@opencode-ai/plugin"
import type { Auth, Provider } from "@opencode-ai/sdk/v2"
import { OAUTH_DUMMY_KEY } from "../../src/auth"
import { createAzureAuthHooks } from "../../src/plugin/azure"
const resourceName = process.env.AZURE_RESOURCE_NAME
afterEach(() => {
if (resourceName === undefined) delete process.env.AZURE_RESOURCE_NAME
else process.env.AZURE_RESOURCE_NAME = resourceName
})
const oauth: Auth = {
type: "oauth",
access: OAUTH_DUMMY_KEY,
refresh: OAUTH_DUMMY_KEY,
expires: Date.now() + 60 * 60 * 1000,
accountId: "test-resource",
}
const provider: Provider = {
id: "azure",
name: "Azure",
source: "custom",
env: [],
options: {},
models: {},
}
function oauthMethod(hooks: Hooks) {
const method = hooks.auth?.methods.find((method) => method.type === "oauth")
if (!method || method.type !== "oauth") throw new Error("Azure OAuth method is missing")
return method
}
function loader(hooks: Hooks) {
if (!hooks.auth?.loader) throw new Error("Azure auth loader is missing")
return hooks.auth.loader
}
function customFetch(options: Record<string, unknown>) {
const result = options["fetch"]
if (typeof result !== "function") throw new Error("Azure custom fetch is missing")
return async (input: RequestInfo | URL, init?: RequestInit) => {
const response: unknown = await Reflect.apply(result, undefined, [input, init])
if (!(response instanceof Response)) throw new Error("Azure custom fetch returned an invalid response")
return response
}
}
function models(...ids: string[]): Provider["models"] {
return Object.fromEntries(
ids.map((id) => [
id,
{
id,
providerID: "azure",
name: id,
family: "",
api: { id, url: "", npm: "@ai-sdk/azure" },
status: "active",
headers: {},
options: {},
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
limit: { context: 0, output: 0 },
capabilities: {
temperature: true,
reasoning: false,
attachment: false,
toolcall: true,
input: { text: true, audio: false, image: false, video: false, pdf: false },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
},
release_date: "",
variants: {},
},
]),
)
}
function azureShell(scopes: string[]) {
return (_strings: TemplateStringsArray, ...values: string[]) => {
const output = {
quiet: () => output,
json: async () => {
const scope = values[0]
scopes.push(scope)
return {
accessToken: `${scope}-token`,
expires_on: Math.floor((Date.now() + 60 * 60 * 1000) / 1000),
}
},
}
return output
}
}
function discoveryShell(accounts: unknown, deployments: unknown, commands: string[]) {
return (strings: TemplateStringsArray, ...values: string[]) => {
const command = String.raw(strings, ...values)
commands.push(command)
const output = {
quiet: () => output,
json: async () => (command.includes("deployment list") ? deployments : accounts),
}
return output
}
}
describe("plugin.azure", () => {
test("keeps the existing API-key method and adds Entra ID", () => {
delete process.env.AZURE_RESOURCE_NAME
const hooks = createAzureAuthHooks(azureShell([]))
expect(hooks.auth?.provider).toBe("azure")
expect(hooks.provider?.id).toBe("azure")
expect(hooks.auth?.methods.map((method) => [method.type, method.label])).toEqual([
["api", "API key"],
["oauth", "Microsoft Entra ID (Azure CLI)"],
])
expect(hooks.auth?.methods[0]).toEqual({
type: "api",
label: "API key",
prompts: [
{
type: "text",
key: "resourceName",
message: "Enter Azure Resource Name",
placeholder: "e.g. my-models",
},
],
})
expect(hooks.auth?.methods[1].prompts).toEqual(hooks.auth?.methods[0].prompts)
})
test("checks Azure CLI and stores the resource name", async () => {
const scopes: string[] = []
const hooks = createAzureAuthHooks(azureShell(scopes))
const authorization = await oauthMethod(hooks).authorize({ resourceName: "test-resource" })
if (authorization.method !== "auto") throw new Error("Unexpected Azure authorization method")
expect(await authorization.callback()).toMatchObject({
type: "success",
access: OAUTH_DUMMY_KEY,
refresh: OAUTH_DUMMY_KEY,
accountId: "test-resource",
})
expect(scopes).toEqual(["https://cognitiveservices.azure.com/.default"])
})
test("discovers deployed models through Azure CLI", async () => {
const commands: string[] = []
const hooks = createAzureAuthHooks(
discoveryShell(
[{ name: "test-resource", resourceGroup: "test-group" }],
[
{
name: "gpt-production",
properties: { model: { name: "gpt-5-mini" }, provisioningState: "Succeeded" },
},
{
name: "DeepSeek-V4-Flash",
properties: { model: { name: "DeepSeek-V4-Flash" }, provisioningState: "Succeeded" },
},
{
name: "phi-production",
properties: { model: { name: "Phi-4-mini-instruct" }, provisioningState: "Succeeded" },
},
{
name: "gpt-5-nano",
properties: { model: { name: "gpt-5-nano" }, provisioningState: "Creating" },
},
],
commands,
),
)
const list = hooks.provider?.models
if (!list) throw new Error("Azure provider model hook is missing")
const result = await list(
{
...provider,
models: models("gpt-5-mini", "deepseek-v4-flash", "phi-4-mini", "phi-4-mini-instruct", "gpt-5-nano"),
},
{ auth: oauth },
)
expect(Object.keys(result)).toEqual(["gpt-5-mini", "deepseek-v4-flash", "phi-4-mini-instruct"])
expect(result["gpt-5-mini"].api.id).toBe("gpt-production")
expect(result["deepseek-v4-flash"].api.id).toBe("DeepSeek-V4-Flash")
expect(result["phi-4-mini-instruct"].api.id).toBe("phi-production")
expect(commands).toEqual([
"az cognitiveservices account list --output json --only-show-errors",
"az cognitiveservices account deployment list --name test-resource --resource-group test-group --output json --only-show-errors",
])
})
test("keeps startup running when Azure discovery fails", async () => {
const hooks = createAzureAuthHooks(() => {
const output = {
quiet: () => output,
json: async () => {
throw new Error("Azure CLI failed")
},
}
return output
})
const list = hooks.provider?.models
if (!list) throw new Error("Azure provider model hook is missing")
expect(await list({ ...provider, models: models("gpt-5-mini") }, { auth: oauth })).toEqual({})
})
test("does not change API-key loading", async () => {
const scopes: string[] = []
const hooks = createAzureAuthHooks(azureShell(scopes))
const catalog = models("gpt-5-mini")
const list = hooks.provider?.models
if (!list) throw new Error("Azure provider model hook is missing")
expect(await loader(hooks)(async () => ({ type: "api", key: "test-key" }), provider)).toEqual({})
expect(await list({ ...provider, models: catalog }, { auth: { type: "api", key: "test-key" } })).toBe(catalog)
expect(scopes).toEqual([])
})
test("uses Azure CLI bearer tokens for Azure inference endpoints", async () => {
const scopes: string[] = []
const requests: Headers[] = []
const hooks = createAzureAuthHooks(azureShell(scopes), async (_input, init) => {
requests.push(new Headers(init?.headers))
return new Response(null, { status: 200 })
})
const options = await loader(hooks)(async () => oauth, provider)
const request = customFetch(options)
await request("https://test-resource.openai.azure.com/openai/v1/responses", {
headers: { "api-key": OAUTH_DUMMY_KEY, "x-keep": "yes" },
})
await request("https://test-resource.services.ai.azure.com/models/chat/completions", {
headers: { Authorization: `Bearer ${OAUTH_DUMMY_KEY}` },
})
await request("https://test-resource.services.ai.azure.com/anthropic/v1/messages", {
headers: { "x-api-key": OAUTH_DUMMY_KEY },
})
expect(scopes).toEqual(["https://cognitiveservices.azure.com/.default", "https://ai.azure.com/.default"])
expect(requests.map((headers) => headers.get("authorization"))).toEqual([
"Bearer https://cognitiveservices.azure.com/.default-token",
"Bearer https://cognitiveservices.azure.com/.default-token",
"Bearer https://ai.azure.com/.default-token",
])
expect(requests[0].get("api-key")).toBeNull()
expect(requests[0].get("x-keep")).toBe("yes")
expect(requests[2].get("x-api-key")).toBeNull()
expect(requests.every((headers) => headers.get("user-agent")?.startsWith("opencode/"))).toBe(true)
})
})
+169 -5
View File
@@ -1,9 +1,13 @@
import { describe, expect, test } from "bun:test"
import { createServer, type IncomingMessage } from "node:http"
import { type AddressInfo } from "node:net"
import { WebSocketServer } from "ws"
import {
CodexAuthPlugin,
parseJwtClaims,
extractAccountIdFromClaims,
extractAccountId,
extractResidency,
renderOAuthError,
type IdTokenClaims,
} from "../../src/plugin/openai/codex"
@@ -131,6 +135,69 @@ describe("plugin.codex", () => {
})
})
describe("extractResidency", () => {
test("extracts compute residency from the namespaced auth claims", () => {
expect(
extractResidency(
createTestJwt({
"https://api.openai.com/auth": { chatgpt_compute_residency: "eu" },
}),
),
).toBe("eu")
})
test("falls back to a root compute residency claim", () => {
expect(extractResidency(createTestJwt({ chatgpt_compute_residency: "us" }))).toBe("us")
})
test("supports compute residency values without maintaining a region list", () => {
expect(
extractResidency(
createTestJwt({
"https://api.openai.com/auth": { chatgpt_compute_residency: "ae" },
}),
),
).toBe("ae")
expect(
extractResidency(
createTestJwt({
"https://api.openai.com/auth": { chatgpt_compute_residency: "future-region_1" },
}),
),
).toBe("future-region_1")
})
test("ignores unconstrained and data residency values", () => {
expect(
extractResidency(
createTestJwt({
"https://api.openai.com/auth": { chatgpt_compute_residency: "no_constraint" },
}),
),
).toBeUndefined()
expect(
extractResidency(
createTestJwt({
"https://api.openai.com/auth": { chatgpt_data_residency: "gb" },
}),
),
).toBeUndefined()
expect(extractResidency(createTestJwt({ chatgpt_compute_residency: "" }))).toBeUndefined()
expect(extractResidency("not-a-jwt")).toBeUndefined()
})
test("prefers a namespaced unconstrained value over a root residency", () => {
expect(
extractResidency(
createTestJwt({
chatgpt_compute_residency: "eu",
"https://api.openai.com/auth": { chatgpt_compute_residency: "no_constraint" },
}),
),
).toBeUndefined()
})
})
test("installs websocket transport only when experimental websockets are enabled", async () => {
const disabled = await CodexAuthPlugin({} as never)
const enabled = await CodexAuthPlugin({} as never, { experimentalWebSockets: true })
@@ -149,6 +216,73 @@ describe("plugin.codex", () => {
await enabled.dispose?.()
})
test("sends token residency only to the ChatGPT Codex backend", async () => {
const requests: Array<{ path: string; residency: string | null }> = []
using server = Bun.serve({
port: 0,
fetch(request) {
requests.push({
path: new URL(request.url).pathname,
residency: request.headers.get("x-openai-internal-codex-residency"),
})
return new Response("{}")
},
})
const hooks = await CodexAuthPlugin({} as never, {
codexApiEndpoint: new URL("/backend-api/codex/responses", server.url).toString(),
})
const loaded = await hooks.auth!.loader!(
async () =>
({
type: "oauth",
refresh: "refresh",
access: createTestJwt({
"https://api.openai.com/auth": { chatgpt_compute_residency: "eu" },
}),
expires: Date.now() + 60_000,
}) as never,
{} as never,
)
await loaded.fetch!("https://api.openai.com/v1/responses")
await loaded.fetch!(new URL("/other", server.url))
expect(requests).toEqual([
{ path: "/backend-api/codex/responses", residency: "eu" },
{ path: "/other", residency: null },
])
})
test("sends token residency through the WebSocket transport", async () => {
await using server = await createCodexWebSocketServer()
const hooks = await CodexAuthPlugin({} as never, {
codexApiEndpoint: server.url,
experimentalWebSockets: true,
})
const loaded = await hooks.auth!.loader!(
async () =>
({
type: "oauth",
refresh: "refresh",
access: createTestJwt({
"https://api.openai.com/auth": { chatgpt_compute_residency: "eu" },
}),
expires: Date.now() + 60_000,
}) as never,
{} as never,
)
const response = await loaded.fetch!("https://api.openai.com/v1/responses", {
method: "POST",
headers: { "session-id": "session-1" },
body: JSON.stringify({ stream: true, input: "hi" }),
})
expect(await response.text()).toContain("data: [DONE]")
expect(server.headers()?.["x-openai-internal-codex-residency"]).toBe("eu")
await hooks.dispose?.()
})
test("filters unsupported modes and uses Codex context limits for OAuth GPT models", async () => {
const hooks = await CodexAuthPlugin({} as never)
const limit = { context: 1_050_000, input: 922_000, output: 128_000 }
@@ -193,6 +327,9 @@ describe("plugin.codex", () => {
})
test("deduplicates concurrent Codex token refreshes", async () => {
const refreshedAccess = createTestJwt({
"https://api.openai.com/auth": { chatgpt_compute_residency: "eu" },
})
let auth = {
type: "oauth" as const,
refresh: "refresh-old",
@@ -207,7 +344,7 @@ describe("plugin.codex", () => {
resolveRefresh = resolve
})
let refreshRequests = 0
const apiRequests: { authorization: string | null; accountId: string | null }[] = []
const apiRequests: { authorization: string | null; accountId: string | null; residency: string | null }[] = []
using server = Bun.serve({
port: 0,
@@ -219,7 +356,7 @@ describe("plugin.codex", () => {
await refreshReady
return Response.json({
id_token: createTestJwt({ chatgpt_account_id: "acc-123" }),
access_token: "access-new",
access_token: refreshedAccess,
refresh_token: "refresh-new",
expires_in: 3600,
})
@@ -229,6 +366,7 @@ describe("plugin.codex", () => {
apiRequests.push({
authorization: request.headers.get("authorization"),
accountId: request.headers.get("ChatGPT-Account-Id"),
residency: request.headers.get("x-openai-internal-codex-residency"),
})
return new Response("{}", { status: 200 })
}
@@ -281,11 +419,11 @@ describe("plugin.codex", () => {
expect(refreshRequests).toBe(1)
expect(authUpdates).toHaveLength(1)
expect(authUpdates[0]?.body.refresh).toBe("refresh-new")
expect(authUpdates[0]?.body.access).toBe("access-new")
expect(authUpdates[0]?.body.access).toBe(refreshedAccess)
expect(authUpdates[0]?.body.accountId).toBe("acc-123")
expect(apiRequests).toEqual([
{ authorization: "Bearer access-new", accountId: "acc-123" },
{ authorization: "Bearer access-new", accountId: "acc-123" },
{ authorization: `Bearer ${refreshedAccess}`, accountId: "acc-123", residency: "eu" },
{ authorization: `Bearer ${refreshedAccess}`, accountId: "acc-123", residency: "eu" },
])
})
})
@@ -297,3 +435,29 @@ async function waitFor(predicate: () => boolean) {
await new Promise((resolve) => setTimeout(resolve, 1))
}
}
async function createCodexWebSocketServer() {
let headers: IncomingMessage["headers"] | undefined
const server = createServer()
const sockets = new WebSocketServer({ server })
sockets.on("connection", (socket, request) => {
headers = request.headers
socket.once("message", () => {
socket.send(JSON.stringify({ type: "response.completed", response: { id: "resp_123" } }))
})
})
await new Promise<void>((resolve, reject) => {
server.once("error", reject)
server.listen(0, "127.0.0.1", resolve)
})
const address = server.address() as AddressInfo
return {
url: `http://127.0.0.1:${address.port}/backend-api/codex/responses`,
headers: () => headers,
async [Symbol.asyncDispose]() {
for (const socket of sockets.clients) socket.terminate()
sockets.close()
server.close()
},
}
}
@@ -17,13 +17,18 @@ describe("plugin.openai.ws", () => {
const socket = await OpenAIWebSocket.connectResponsesWebSocket({
url: server.wsUrl,
headers: { authorization: "Bearer test", "content-length": "123" },
headers: {
authorization: "Bearer test",
"content-length": "123",
"x-openai-internal-codex-residency": "eu",
},
})
expect(OpenAIWebSocket.toWebSocketUrl("http://example.com/v1/responses")).toBe("ws://example.com/v1/responses")
expect(OpenAIWebSocket.toWebSocketUrl("https://example.com/v1/responses")).toBe("wss://example.com/v1/responses")
expect(headers?.authorization).toBe("Bearer test")
expect(headers?.["openai-beta"]).toBe(OpenAIWebSocket.PROTOCOL_HEADER)
expect(headers?.["x-openai-internal-codex-residency"]).toBe("eu")
expect(headers?.["content-length"]).toBeUndefined()
socket.terminate()
})
@@ -232,6 +237,26 @@ describe("plugin.openai.ws-pool", () => {
fetch.close()
})
test("falls back immediately to HTTP when a websocket request is too large", async () => {
let connections = 0
await using server = await createWebSocketServer((socket) => {
connections += 1
socket.once("message", () => socket.close(1009, "payload too large"))
})
const fetch = OpenAIWebSocketPool.createWebSocketFetch({
url: server.url,
})
const first = await fetch(server.url, streamRequest())
const second = await fetch(server.url, streamRequest())
expect(await first.text()).toBe("http")
expect(await second.text()).toBe("http")
expect(connections).toBe(1)
expect(server.httpRequests).toHaveLength(2)
fetch.close()
})
test("removes HTTP fallback when its session is deleted", async () => {
let websocketAttempts = 0
await using server = await createRejectingWebSocketServer(() => websocketAttempts++)
+3 -3
View File
@@ -104,7 +104,7 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 1,050 | 2,600 | 5,200 |
| DeepSeek V4 Flash | 3,800 | 9,450 | 18,900 |
| DeepSeek V4 Flash | 7,600 | 18,900 | 37,800 |
| Hy3 | 4,300 | 10,750 | 21,500 |
تستند التقديرات إلى أنماط الطلبات المرصودة:
@@ -152,8 +152,8 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال
| Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 |
| DeepSeek V4 Pro (Off-Peak) | $0.66 | $1.98 | $0.022 | - | $15 |
| DeepSeek V4 Pro (Peak) | $1.32 | $3.96 | $0.044 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $15 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $30 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $30 |
| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 |
**DeepSeek V4 Flash / Pro:** ساعات Peak هي 01:00-04:00 و06:00-10:00 UTC؛ وجميع الساعات الأخرى Off-Peak. [اعرف المزيد](https://api-docs.deepseek.com/quick_start/pricing/).
+3 -3
View File
@@ -114,7 +114,7 @@ Tabela ispod pruža procijenjeni broj zahtjeva na osnovu tipičnih obrazaca kori
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 1,050 | 2,600 | 5,200 |
| DeepSeek V4 Flash | 3,800 | 9,450 | 18,900 |
| DeepSeek V4 Flash | 7,600 | 18,900 | 37,800 |
| Hy3 | 4,300 | 10,750 | 21,500 |
Procjene se zasnivaju na zapaženim obrascima zahtjeva:
@@ -162,8 +162,8 @@ Procjene se također zasnivaju na sljedećim cijenama po 1M tokena i mjesečnoj
| Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 |
| DeepSeek V4 Pro (Off-Peak) | $0.66 | $1.98 | $0.022 | - | $15 |
| DeepSeek V4 Pro (Peak) | $1.32 | $3.96 | $0.044 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $15 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $30 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $30 |
| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 |
**DeepSeek V4 Flash / Pro:** Peak sati su 01:00-04:00 i 06:00-10:00 UTC; svi ostali sati su Off-Peak. [Saznajte više](https://api-docs.deepseek.com/quick_start/pricing/).
+3 -3
View File
@@ -114,7 +114,7 @@ Tabellen nedenfor giver et estimeret antal anmodninger baseret på typiske Go-fo
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 1,050 | 2,600 | 5,200 |
| DeepSeek V4 Flash | 3,800 | 9,450 | 18,900 |
| DeepSeek V4 Flash | 7,600 | 18,900 | 37,800 |
| Hy3 | 4,300 | 10,750 | 21,500 |
Estimaterne er baseret på observerede anmodningsmønstre:
@@ -162,8 +162,8 @@ Estimaterne er også baseret på følgende priser pr. 1M tokens og det månedlig
| Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 |
| DeepSeek V4 Pro (Off-Peak) | $0.66 | $1.98 | $0.022 | - | $15 |
| DeepSeek V4 Pro (Peak) | $1.32 | $3.96 | $0.044 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $15 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $30 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $30 |
| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 |
**DeepSeek V4 Flash / Pro:** Peak-tiderne er 01:00-04:00 og 06:00-10:00 UTC; alle andre tider er Off-Peak. [Læs mere](https://api-docs.deepseek.com/quick_start/pricing/).
+3 -3
View File
@@ -106,7 +106,7 @@ Die folgende Tabelle zeigt eine geschätzte Anzahl von Anfragen basierend auf ty
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 1,050 | 2,600 | 5,200 |
| DeepSeek V4 Flash | 3,800 | 9,450 | 18,900 |
| DeepSeek V4 Flash | 7,600 | 18,900 | 37,800 |
| Hy3 | 4,300 | 10,750 | 21,500 |
Die Schätzungen basieren auf beobachteten Anfragemustern:
@@ -154,8 +154,8 @@ Die Schätzungen basieren außerdem auf den folgenden Preisen pro 1M Tokens und
| Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 |
| DeepSeek V4 Pro (Off-Peak) | $0.66 | $1.98 | $0.022 | - | $15 |
| DeepSeek V4 Pro (Peak) | $1.32 | $3.96 | $0.044 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $15 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $30 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $30 |
| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 |
**DeepSeek V4 Flash / Pro:** Die Peak-Zeiten sind 01:00-04:00 und 06:00-10:00 UTC; alle anderen Zeiten sind Off-Peak. [Mehr erfahren](https://api-docs.deepseek.com/quick_start/pricing/).
+3 -3
View File
@@ -114,7 +114,7 @@ La siguiente tabla proporciona una cantidad estimada de peticiones basada en los
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 1,050 | 2,600 | 5,200 |
| DeepSeek V4 Flash | 3,800 | 9,450 | 18,900 |
| DeepSeek V4 Flash | 7,600 | 18,900 | 37,800 |
| Hy3 | 4,300 | 10,750 | 21,500 |
Las estimaciones se basan en los patrones de peticiones observados:
@@ -162,8 +162,8 @@ Las estimaciones también se basan en los siguientes precios por 1M tokens y en
| Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 |
| DeepSeek V4 Pro (Off-Peak) | $0.66 | $1.98 | $0.022 | - | $15 |
| DeepSeek V4 Pro (Peak) | $1.32 | $3.96 | $0.044 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $15 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $30 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $30 |
| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 |
**DeepSeek V4 Flash / Pro:** Las horas Peak son 01:00-04:00 y 06:00-10:00 UTC; todas las demás horas son Off-Peak. [Más información](https://api-docs.deepseek.com/quick_start/pricing/).
+3 -3
View File
@@ -104,7 +104,7 @@ Le tableau ci-dessous fournit une estimation du nombre de requêtes basée sur d
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 1,050 | 2,600 | 5,200 |
| DeepSeek V4 Flash | 3,800 | 9,450 | 18,900 |
| DeepSeek V4 Flash | 7,600 | 18,900 | 37,800 |
| Hy3 | 4,300 | 10,750 | 21,500 |
Les estimations sont basées sur les schémas de requêtes observés :
@@ -152,8 +152,8 @@ Les estimations sont également basées sur les prix suivants par 1M tokens et s
| Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 |
| DeepSeek V4 Pro (Off-Peak) | $0.66 | $1.98 | $0.022 | - | $15 |
| DeepSeek V4 Pro (Peak) | $1.32 | $3.96 | $0.044 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $15 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $30 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $30 |
| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 |
**DeepSeek V4 Flash / Pro:** Les heures Peak sont 01:00-04:00 et 06:00-10:00 UTC ; toutes les autres heures sont Off-Peak. [En savoir plus](https://api-docs.deepseek.com/quick_start/pricing/).
+3 -3
View File
@@ -114,7 +114,7 @@ The table below provides an estimated request count based on typical Go usage pa
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 1,050 | 2,600 | 5,200 |
| DeepSeek V4 Flash | 3,800 | 9,450 | 18,900 |
| DeepSeek V4 Flash | 7,600 | 18,900 | 37,800 |
| Hy3 | 4,300 | 10,750 | 21,500 |
The estimates are based on observed request patterns:
@@ -162,8 +162,8 @@ The estimates are also based on the following prices per 1M tokens and the month
| Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 |
| DeepSeek V4 Pro (Off-Peak) | $0.66 | $1.98 | $0.022 | - | $15 |
| DeepSeek V4 Pro (Peak) | $1.32 | $3.96 | $0.044 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $15 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $30 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $30 |
| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 |
**DeepSeek V4 Flash / Pro:** Peak hours are 01:00-04:00 and 06:00-10:00 UTC; all other hours are Off-Peak. [Learn more](https://api-docs.deepseek.com/quick_start/pricing/).
+3 -3
View File
@@ -112,7 +112,7 @@ La tabella seguente fornisce una stima del conteggio delle richieste in base a p
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 1,050 | 2,600 | 5,200 |
| DeepSeek V4 Flash | 3,800 | 9,450 | 18,900 |
| DeepSeek V4 Flash | 7,600 | 18,900 | 37,800 |
| Hy3 | 4,300 | 10,750 | 21,500 |
Le stime si basano sui pattern di richieste osservati:
@@ -160,8 +160,8 @@ Le stime si basano anche sui seguenti prezzi per 1M token e sull'utilizzo mensil
| Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 |
| DeepSeek V4 Pro (Off-Peak) | $0.66 | $1.98 | $0.022 | - | $15 |
| DeepSeek V4 Pro (Peak) | $1.32 | $3.96 | $0.044 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $15 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $30 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $30 |
| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 |
**DeepSeek V4 Flash / Pro:** Gli orari Peak sono 01:00-04:00 e 06:00-10:00 UTC; tutti gli altri orari sono Off-Peak. [Scopri di più](https://api-docs.deepseek.com/quick_start/pricing/).
+3 -3
View File
@@ -104,7 +104,7 @@ OpenCode Goには以下の制限が含まれています:
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 1,050 | 2,600 | 5,200 |
| DeepSeek V4 Flash | 3,800 | 9,450 | 18,900 |
| DeepSeek V4 Flash | 7,600 | 18,900 | 37,800 |
| Hy3 | 4,300 | 10,750 | 21,500 |
推定値は、観測されたリクエストパターンに基づいています:
@@ -152,8 +152,8 @@ OpenCode Goには以下の制限が含まれています:
| Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 |
| DeepSeek V4 Pro (Off-Peak) | $0.66 | $1.98 | $0.022 | - | $15 |
| DeepSeek V4 Pro (Peak) | $1.32 | $3.96 | $0.044 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $15 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $30 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $30 |
| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 |
**DeepSeek V4 Flash / Pro:** Peak時間は01:00-04:00と06:00-10:00 UTCで、それ以外の時間はすべてOff-Peakです。[詳しく見る](https://api-docs.deepseek.com/quick_start/pricing/)。
+3 -3
View File
@@ -104,7 +104,7 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다.
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 1,050 | 2,600 | 5,200 |
| DeepSeek V4 Flash | 3,800 | 9,450 | 18,900 |
| DeepSeek V4 Flash | 7,600 | 18,900 | 37,800 |
| Hy3 | 4,300 | 10,750 | 21,500 |
이 예상치는 관찰된 요청 패턴을 기준으로 합니다.
@@ -152,8 +152,8 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다.
| Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 |
| DeepSeek V4 Pro (Off-Peak) | $0.66 | $1.98 | $0.022 | - | $15 |
| DeepSeek V4 Pro (Peak) | $1.32 | $3.96 | $0.044 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $15 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $30 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $30 |
| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 |
**DeepSeek V4 Flash / Pro:** Peak 시간은 01:00-04:00 및 06:00-10:00 UTC이며, 그 외 모든 시간은 Off-Peak입니다. [자세히 알아보기](https://api-docs.deepseek.com/quick_start/pricing/).
+3 -3
View File
@@ -114,7 +114,7 @@ Tabellen nedenfor gir et estimert antall forespørsler basert på typiske bruksm
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 1,050 | 2,600 | 5,200 |
| DeepSeek V4 Flash | 3,800 | 9,450 | 18,900 |
| DeepSeek V4 Flash | 7,600 | 18,900 | 37,800 |
| Hy3 | 4,300 | 10,750 | 21,500 |
Estimatene er basert på observerte forespørselsmønstre:
@@ -162,8 +162,8 @@ Estimatene er også basert på følgende priser per 1M tokens og den månedlige
| Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 |
| DeepSeek V4 Pro (Off-Peak) | $0.66 | $1.98 | $0.022 | - | $15 |
| DeepSeek V4 Pro (Peak) | $1.32 | $3.96 | $0.044 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $15 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $30 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $30 |
| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 |
**DeepSeek V4 Flash / Pro:** Peak-tidene er 01:00-04:00 og 06:00-10:00 UTC; alle andre tider er Off-Peak. [Les mer](https://api-docs.deepseek.com/quick_start/pricing/).
+3 -3
View File
@@ -108,7 +108,7 @@ Poniższa tabela przedstawia szacunkową liczbę żądań na podstawie typowych
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 1,050 | 2,600 | 5,200 |
| DeepSeek V4 Flash | 3,800 | 9,450 | 18,900 |
| DeepSeek V4 Flash | 7,600 | 18,900 | 37,800 |
| Hy3 | 4,300 | 10,750 | 21,500 |
Szacunki te opierają się na zaobserwowanych wzorcach żądań:
@@ -156,8 +156,8 @@ Szacunki opierają się również na następujących cenach za 1M tokenów oraz
| Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 |
| DeepSeek V4 Pro (Off-Peak) | $0.66 | $1.98 | $0.022 | - | $15 |
| DeepSeek V4 Pro (Peak) | $1.32 | $3.96 | $0.044 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $15 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $30 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $30 |
| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 |
**DeepSeek V4 Flash / Pro:** Godziny Peak to 01:00-04:00 i 06:00-10:00 UTC; wszystkie pozostałe godziny to Off-Peak. [Dowiedz się więcej](https://api-docs.deepseek.com/quick_start/pricing/).
+4 -8
View File
@@ -457,14 +457,6 @@ If you encounter "I'm sorry, but I cannot assist with that request" errors, try
/models
```
#### Microsoft Entra ID (Azure CLI)
You can use your Azure CLI session instead of an API key. [Install the Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli), run `az login`, then select **Microsoft Entra ID (Azure CLI)** when connecting the **Azure** provider and enter the same Resource name.
OpenCode finds the Resource group and discovers its deployed models from the active Azure CLI subscription. Run `az account set --subscription NAME_OR_ID` first if the Resource is in a different subscription.
Assign your identity the inference role required by the deployment: **Cognitive Services OpenAI User** for Azure OpenAI models or **Cognitive Services User** for other Foundry models. OpenCode refreshes access tokens through the Azure CLI, so you only need to sign in again when the CLI session expires.
---
### Azure Cognitive Services
@@ -1726,6 +1718,10 @@ We recommend signing up for [ChatGPT Plus or Pro](https://chatgpt.com/pricing).
/models
```
##### Compute residency
For ChatGPT OAuth, OpenCode automatically applies a regional inference residency requirement when one is advertised by your workspace credentials. It forwards the compute residency value from the credential instead of maintaining a fixed list of regions. Data residency at rest does not imply regional inference.
##### Using API keys
If you already have an API key, you can select **Manually enter API Key** and paste it in your terminal.
+3 -3
View File
@@ -114,7 +114,7 @@ A tabela abaixo fornece uma contagem estimada de requisições com base nos padr
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 1,050 | 2,600 | 5,200 |
| DeepSeek V4 Flash | 3,800 | 9,450 | 18,900 |
| DeepSeek V4 Flash | 7,600 | 18,900 | 37,800 |
| Hy3 | 4,300 | 10,750 | 21,500 |
As estimativas se baseiam nos padrões de requisições observados:
@@ -162,8 +162,8 @@ As estimativas também se baseiam nos seguintes preços por 1M tokens e no uso m
| Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 |
| DeepSeek V4 Pro (Off-Peak) | $0.66 | $1.98 | $0.022 | - | $15 |
| DeepSeek V4 Pro (Peak) | $1.32 | $3.96 | $0.044 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $15 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $30 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $30 |
| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 |
**DeepSeek V4 Flash / Pro:** Os horários Peak são 01:00-04:00 e 06:00-10:00 UTC; todos os demais horários são Off-Peak. [Saiba mais](https://api-docs.deepseek.com/quick_start/pricing/).
+3 -3
View File
@@ -114,7 +114,7 @@ OpenCode Go включает следующие лимиты:
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 1,050 | 2,600 | 5,200 |
| DeepSeek V4 Flash | 3,800 | 9,450 | 18,900 |
| DeepSeek V4 Flash | 7,600 | 18,900 | 37,800 |
| Hy3 | 4,300 | 10,750 | 21,500 |
Эти оценки основаны на наблюдаемых показателях запросов:
@@ -162,8 +162,8 @@ OpenCode Go включает следующие лимиты:
| Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 |
| DeepSeek V4 Pro (Off-Peak) | $0.66 | $1.98 | $0.022 | - | $15 |
| DeepSeek V4 Pro (Peak) | $1.32 | $3.96 | $0.044 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $15 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $30 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $30 |
| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 |
**DeepSeek V4 Flash / Pro:** Часы Peak: 01:00-04:00 и 06:00-10:00 UTC; все остальные часы относятся к Off-Peak. [Подробнее](https://api-docs.deepseek.com/quick_start/pricing/).
+3 -3
View File
@@ -104,7 +104,7 @@ OpenCode Go มีขีดจำกัดดังต่อไปนี้:
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 1,050 | 2,600 | 5,200 |
| DeepSeek V4 Flash | 3,800 | 9,450 | 18,900 |
| DeepSeek V4 Flash | 7,600 | 18,900 | 37,800 |
| Hy3 | 4,300 | 10,750 | 21,500 |
การประมาณการนี้อ้างอิงจากรูปแบบการใช้งาน request ที่สังเกตพบ:
@@ -152,8 +152,8 @@ OpenCode Go มีขีดจำกัดดังต่อไปนี้:
| Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 |
| DeepSeek V4 Pro (Off-Peak) | $0.66 | $1.98 | $0.022 | - | $15 |
| DeepSeek V4 Pro (Peak) | $1.32 | $3.96 | $0.044 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $15 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $30 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $30 |
| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 |
**DeepSeek V4 Flash / Pro:** ช่วงเวลา Peak คือ 01:00-04:00 และ 06:00-10:00 UTC ส่วนเวลาอื่นทั้งหมดเป็น Off-Peak [ดูข้อมูลเพิ่มเติม](https://api-docs.deepseek.com/quick_start/pricing/)
+3 -3
View File
@@ -104,7 +104,7 @@ Aşağıdaki tablo, tipik Go kullanım modellerine dayalı tahmini bir istek say
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 1,050 | 2,600 | 5,200 |
| DeepSeek V4 Flash | 3,800 | 9,450 | 18,900 |
| DeepSeek V4 Flash | 7,600 | 18,900 | 37,800 |
| Hy3 | 4,300 | 10,750 | 21,500 |
Tahminler, gözlemlenen istek modellerine dayanır:
@@ -152,8 +152,8 @@ Tahminler ayrıca 1M token başına aşağıdaki fiyatlara ve her modelle birlik
| Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 |
| DeepSeek V4 Pro (Off-Peak) | $0.66 | $1.98 | $0.022 | - | $15 |
| DeepSeek V4 Pro (Peak) | $1.32 | $3.96 | $0.044 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $15 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $30 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $30 |
| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 |
**DeepSeek V4 Flash / Pro:** Peak saatleri 01:00-04:00 ve 06:00-10:00 UTC'dir; diğer tüm saatler Off-Peak'tir. [Daha fazla bilgi](https://api-docs.deepseek.com/quick_start/pricing/).
+3 -3
View File
@@ -104,7 +104,7 @@ OpenCode Go 包含以下限制:
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 1,050 | 2,600 | 5,200 |
| DeepSeek V4 Flash | 3,800 | 9,450 | 18,900 |
| DeepSeek V4 Flash | 7,600 | 18,900 | 37,800 |
| Hy3 | 4,300 | 10,750 | 21,500 |
预估值基于观察到的请求模式:
@@ -152,8 +152,8 @@ OpenCode Go 包含以下限制:
| Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 |
| DeepSeek V4 Pro (Off-Peak) | $0.66 | $1.98 | $0.022 | - | $15 |
| DeepSeek V4 Pro (Peak) | $1.32 | $3.96 | $0.044 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $15 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $30 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $30 |
| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 |
**DeepSeek V4 Flash / Pro:** Peak 时段为 01:00-04:00 和 06:00-10:00 UTC;其他所有时段均为 Off-Peak。[了解更多](https://api-docs.deepseek.com/quick_start/pricing/)。
+3 -3
View File
@@ -104,7 +104,7 @@ OpenCode Go 包含以下限制:
| Qwen3.7 Plus | 4,300 | 10,800 | 21,600 |
| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 |
| DeepSeek V4 Pro | 1,050 | 2,600 | 5,200 |
| DeepSeek V4 Flash | 3,800 | 9,450 | 18,900 |
| DeepSeek V4 Flash | 7,600 | 18,900 | 37,800 |
| Hy3 | 4,300 | 10,750 | 21,500 |
這些預估值是基於觀察到的請求模式:
@@ -152,8 +152,8 @@ OpenCode Go 包含以下限制:
| Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 |
| DeepSeek V4 Pro (Off-Peak) | $0.66 | $1.98 | $0.022 | - | $15 |
| DeepSeek V4 Pro (Peak) | $1.32 | $3.96 | $0.044 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $15 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $15 |
| DeepSeek V4 Flash (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $30 |
| DeepSeek V4 Flash (Peak) | $0.44 | $1.32 | $0.014 | - | $30 |
| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 |
**DeepSeek V4 Flash / Pro:** Peak 時段為 01:00-04:00 和 06:00-10:00 UTC;其他所有時段均為 Off-Peak。[了解更多](https://api-docs.deepseek.com/quick_start/pricing/)。
-3
View File
@@ -38,9 +38,6 @@ await prepareReleaseFiles()
console.log("\n=== cli ===\n")
await $`bun ./packages/opencode/script/publish.ts`
console.log("\n=== preview cli ===\n")
await $`bun ./packages/cli/script/publish.ts`
console.log("\n=== sdk ===\n")
await $`bun ./packages/sdk/js/script/publish.ts`