dsh-coding-subscription-oauth 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +136 -0
- package/CONTRIBUTING.md +109 -0
- package/INSTALL.md +220 -0
- package/LICENSE +19 -0
- package/NOTICE +11 -0
- package/README.de.md +254 -0
- package/README.es.md +254 -0
- package/README.fr.md +254 -0
- package/README.ja.md +254 -0
- package/README.ko.md +254 -0
- package/README.md +286 -0
- package/README.pt-BR.md +254 -0
- package/README.ru.md +254 -0
- package/README.zh-CN.md +271 -0
- package/cordis.patch.yml +13 -0
- package/docs/00-project-rules.md +195 -0
- package/docs/02-architecture.md +132 -0
- package/docs/02-architecture.zh-CN.md +128 -0
- package/lib/adapter.d.ts +24 -0
- package/lib/adapter.d.ts.map +1 -0
- package/lib/alias-adapter.d.ts +43 -0
- package/lib/alias-adapter.d.ts.map +1 -0
- package/lib/auth-routes.d.ts +155 -0
- package/lib/auth-routes.d.ts.map +1 -0
- package/lib/auth.d.ts +29 -0
- package/lib/auth.d.ts.map +1 -0
- package/lib/bin.d.ts +6 -0
- package/lib/bin.d.ts.map +1 -0
- package/lib/bin.js +27591 -0
- package/lib/bin.js.map +7 -0
- package/lib/capability-routes.d.ts +43 -0
- package/lib/capability-routes.d.ts.map +1 -0
- package/lib/capability-runtime.d.ts +56 -0
- package/lib/capability-runtime.d.ts.map +1 -0
- package/lib/capability-settings.d.ts +263 -0
- package/lib/capability-settings.d.ts.map +1 -0
- package/lib/capability-tools.d.ts +50 -0
- package/lib/capability-tools.d.ts.map +1 -0
- package/lib/catalog.d.ts +53 -0
- package/lib/catalog.d.ts.map +1 -0
- package/lib/client.js +3 -0
- package/lib/client.js.map +7 -0
- package/lib/codex-http.d.ts +77 -0
- package/lib/codex-http.d.ts.map +1 -0
- package/lib/codex-images.d.ts +109 -0
- package/lib/codex-images.d.ts.map +1 -0
- package/lib/codex-model-capabilities.d.ts +112 -0
- package/lib/codex-model-capabilities.d.ts.map +1 -0
- package/lib/codex-search.d.ts +96 -0
- package/lib/codex-search.d.ts.map +1 -0
- package/lib/codex-usage.d.ts +79 -0
- package/lib/codex-usage.d.ts.map +1 -0
- package/lib/gateway-anthropic-messages.d.ts +8 -0
- package/lib/gateway-anthropic-messages.d.ts.map +1 -0
- package/lib/gateway-auth.d.ts +22 -0
- package/lib/gateway-auth.d.ts.map +1 -0
- package/lib/gateway-backend.d.ts +62 -0
- package/lib/gateway-backend.d.ts.map +1 -0
- package/lib/gateway-body.d.ts +9 -0
- package/lib/gateway-body.d.ts.map +1 -0
- package/lib/gateway-config.d.ts +24 -0
- package/lib/gateway-config.d.ts.map +1 -0
- package/lib/gateway-http.d.ts +16 -0
- package/lib/gateway-http.d.ts.map +1 -0
- package/lib/gateway-openai-chat.d.ts +8 -0
- package/lib/gateway-openai-chat.d.ts.map +1 -0
- package/lib/gateway-openai-responses.d.ts +8 -0
- package/lib/gateway-openai-responses.d.ts.map +1 -0
- package/lib/gateway-parse.d.ts +10 -0
- package/lib/gateway-parse.d.ts.map +1 -0
- package/lib/gateway-protocol.d.ts +47 -0
- package/lib/gateway-protocol.d.ts.map +1 -0
- package/lib/gateway-routes.d.ts +21 -0
- package/lib/gateway-routes.d.ts.map +1 -0
- package/lib/gateway.d.ts +48 -0
- package/lib/gateway.d.ts.map +1 -0
- package/lib/grok-imagine.d.ts +271 -0
- package/lib/grok-imagine.d.ts.map +1 -0
- package/lib/grok-import.d.ts +21 -0
- package/lib/grok-import.d.ts.map +1 -0
- package/lib/http-json.d.ts +10 -0
- package/lib/http-json.d.ts.map +1 -0
- package/lib/ids.d.ts +33 -0
- package/lib/ids.d.ts.map +1 -0
- package/lib/imagine-routes.d.ts +59 -0
- package/lib/imagine-routes.d.ts.map +1 -0
- package/lib/index.d.ts +69 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +35355 -0
- package/lib/index.js.map +7 -0
- package/lib/invariant.d.ts +9 -0
- package/lib/invariant.d.ts.map +1 -0
- package/lib/invariant.js +14 -0
- package/lib/invariant.js.map +7 -0
- package/lib/kimi-errors.d.ts +13 -0
- package/lib/kimi-errors.d.ts.map +1 -0
- package/lib/media-store.d.ts +130 -0
- package/lib/media-store.d.ts.map +1 -0
- package/lib/oauth-import-routes.d.ts +52 -0
- package/lib/oauth-import-routes.d.ts.map +1 -0
- package/lib/oauth-providers.d.ts +26 -0
- package/lib/oauth-providers.d.ts.map +1 -0
- package/lib/oauth-session.d.ts +40 -0
- package/lib/oauth-session.d.ts.map +1 -0
- package/lib/oauth-sources.d.ts +205 -0
- package/lib/oauth-sources.d.ts.map +1 -0
- package/lib/oauth.d.ts +79 -0
- package/lib/oauth.d.ts.map +1 -0
- package/lib/provider.d.ts +38 -0
- package/lib/provider.d.ts.map +1 -0
- package/lib/proxy.d.ts +17 -0
- package/lib/proxy.d.ts.map +1 -0
- package/lib/redact.d.ts +5 -0
- package/lib/redact.d.ts.map +1 -0
- package/lib/session.d.ts +40 -0
- package/lib/session.d.ts.map +1 -0
- package/lib/store.d.ts +46 -0
- package/lib/store.d.ts.map +1 -0
- package/lib/web-origin.d.ts +10 -0
- package/lib/web-origin.d.ts.map +1 -0
- package/lib/web-routes.d.ts +20 -0
- package/lib/web-routes.d.ts.map +1 -0
- package/package.json +185 -0
- package/patches/dsh-agy@0.1.2.patch +25 -0
- package/scripts/release.mjs +166 -0
- package/scripts/smoke-deployed-routes.mjs +146 -0
- package/scripts/verify-deployed-catalog.mjs +87 -0
- package/src/adapter.ts +282 -0
- package/src/alias-adapter.ts +152 -0
- package/src/auth-routes.ts +871 -0
- package/src/auth.ts +67 -0
- package/src/bin.ts +350 -0
- package/src/capability-routes.ts +275 -0
- package/src/capability-runtime.ts +313 -0
- package/src/capability-settings.ts +657 -0
- package/src/capability-tools.ts +666 -0
- package/src/catalog.ts +271 -0
- package/src/client/GrokBuildSettings.tsx +2221 -0
- package/src/client/index.tsx +37 -0
- package/src/client/locales.ts +421 -0
- package/src/codex-http.ts +447 -0
- package/src/codex-images.ts +485 -0
- package/src/codex-model-capabilities.ts +320 -0
- package/src/codex-search.ts +245 -0
- package/src/codex-usage.ts +263 -0
- package/src/gateway-anthropic-messages.ts +84 -0
- package/src/gateway-auth.ts +100 -0
- package/src/gateway-backend.ts +274 -0
- package/src/gateway-body.ts +49 -0
- package/src/gateway-config.ts +76 -0
- package/src/gateway-http.ts +104 -0
- package/src/gateway-openai-chat.ts +124 -0
- package/src/gateway-openai-responses.ts +53 -0
- package/src/gateway-parse.ts +224 -0
- package/src/gateway-protocol.ts +52 -0
- package/src/gateway-routes.ts +152 -0
- package/src/gateway.ts +242 -0
- package/src/grok-imagine.ts +1627 -0
- package/src/grok-import.ts +151 -0
- package/src/http-json.ts +82 -0
- package/src/ids.ts +45 -0
- package/src/imagine-routes.ts +461 -0
- package/src/index.ts +598 -0
- package/src/invariant.ts +17 -0
- package/src/kimi-errors.ts +26 -0
- package/src/media-store.ts +927 -0
- package/src/oauth-import-routes.ts +314 -0
- package/src/oauth-providers.ts +152 -0
- package/src/oauth-session.ts +183 -0
- package/src/oauth-sources.ts +1104 -0
- package/src/oauth.ts +620 -0
- package/src/provider.ts +128 -0
- package/src/proxy.ts +99 -0
- package/src/redact.ts +72 -0
- package/src/session.ts +218 -0
- package/src/store.ts +217 -0
- package/src/web-origin.ts +60 -0
- package/src/web-routes.ts +75 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
const base = new URL(process.env.DSH_WEB_URL ?? "http://127.0.0.1:3080");
|
|
6
|
+
const origin = base.origin;
|
|
7
|
+
const timeoutMs = Number.parseInt(process.env.DSH_SMOKE_TIMEOUT_MS ?? "180000", 10);
|
|
8
|
+
const restoreProvider = process.env.DSH_RESTORE_PROVIDER;
|
|
9
|
+
const restoreModel = process.env.DSH_RESTORE_MODEL;
|
|
10
|
+
if (restoreProvider === undefined || restoreModel === undefined) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
"Set DSH_RESTORE_PROVIDER and DSH_RESTORE_MODEL before running; smoke selection updates the saved default temporarily",
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
const restoreSelection = {
|
|
16
|
+
provider: restoreProvider,
|
|
17
|
+
model: restoreModel,
|
|
18
|
+
...(process.env.DSH_RESTORE_REASONING === undefined ? {} : { reasoningEffort: process.env.DSH_RESTORE_REASONING }),
|
|
19
|
+
};
|
|
20
|
+
const cases = [
|
|
21
|
+
{
|
|
22
|
+
route: "codex-oauth",
|
|
23
|
+
model: process.env.DSH_CODEX_SMOKE_MODEL ?? "gpt-5.6-sol",
|
|
24
|
+
marker: "DSH_CODEX_OAUTH_SMOKE_OK",
|
|
25
|
+
},
|
|
26
|
+
{ route: "kimi-code-oauth", model: process.env.DSH_KIMI_SMOKE_MODEL ?? "k3", marker: "DSH_KIMI_OAUTH_SMOKE_OK" },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
async function rpc(method, payload) {
|
|
30
|
+
const rpcId = randomUUID();
|
|
31
|
+
const response = await fetch(new URL(`/api/${method}`, base), {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: { accept: "application/json", "content-type": "application/json", origin },
|
|
34
|
+
body: JSON.stringify({ type: "client-request", rpcId, method, payload }),
|
|
35
|
+
signal: AbortSignal.timeout(30_000),
|
|
36
|
+
});
|
|
37
|
+
const envelope = await response.json().catch(() => undefined);
|
|
38
|
+
if (!response.ok) throw new Error(`${method}: HTTP ${response.status}`);
|
|
39
|
+
if (envelope?.rpcId !== rpcId || envelope?.result?.ok !== true) {
|
|
40
|
+
throw new Error(`${method}: ${envelope?.result?.error?.message ?? "invalid response"}`);
|
|
41
|
+
}
|
|
42
|
+
return envelope.result.value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function waitForIdle(sessionId) {
|
|
46
|
+
const deadline = Date.now() + timeoutMs;
|
|
47
|
+
while (Date.now() < deadline) {
|
|
48
|
+
const { items } = await rpc("session.list", {});
|
|
49
|
+
const session = items.find((item) => item.sessionId === sessionId);
|
|
50
|
+
if (session === undefined) throw new Error(`session ${sessionId} disappeared`);
|
|
51
|
+
if (!session.blank && !session.running) return;
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, 1_000));
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`session ${sessionId} did not finish within ${timeoutMs}ms`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function contentBlocks(value, output = []) {
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
for (const item of value) contentBlocks(item, output);
|
|
60
|
+
} else if (value !== null && typeof value === "object") {
|
|
61
|
+
if (typeof value.type === "string" && ["tool-call", "tool-result"].includes(value.type)) output.push(value);
|
|
62
|
+
for (const child of Object.values(value)) contentBlocks(child, output);
|
|
63
|
+
}
|
|
64
|
+
return output;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function runSmoke(testCase) {
|
|
68
|
+
const created = await rpc("session.create", { cwd: process.cwd() });
|
|
69
|
+
const sessionId = created.sessionId;
|
|
70
|
+
let selected = false;
|
|
71
|
+
let failure;
|
|
72
|
+
try {
|
|
73
|
+
await rpc("session.selectModel", {
|
|
74
|
+
sessionId,
|
|
75
|
+
provider: testCase.route,
|
|
76
|
+
model: testCase.model,
|
|
77
|
+
reasoningEffort: "low",
|
|
78
|
+
});
|
|
79
|
+
selected = true;
|
|
80
|
+
await rpc("session.prompt", {
|
|
81
|
+
sessionId,
|
|
82
|
+
mode: "queue",
|
|
83
|
+
clientTimeZone: "UTC",
|
|
84
|
+
content: [
|
|
85
|
+
{
|
|
86
|
+
type: "text",
|
|
87
|
+
text: `Automated OAuth smoke test. You MUST call the glob tool exactly once with pattern "package.json" in the current workspace. After receiving the tool result, reply with exactly ${testCase.marker} and no other text.`,
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
await waitForIdle(sessionId);
|
|
92
|
+
const history = await rpc("session.history", { sessionId, maxMessages: 200 });
|
|
93
|
+
const serialized = JSON.stringify(history.events);
|
|
94
|
+
const blocks = contentBlocks(history.events);
|
|
95
|
+
const toolCalls = blocks.filter((block) => block.type === "tool-call").length;
|
|
96
|
+
const toolResults = blocks.filter((block) => block.type === "tool-result").length;
|
|
97
|
+
const eventTypes = [...new Set(history.events.map((entry) => entry.event.type))];
|
|
98
|
+
const turnErrors = eventTypes.filter((type) => type.includes("error"));
|
|
99
|
+
if (!serialized.includes(testCase.marker)) throw new Error(`${testCase.route}: response marker missing`);
|
|
100
|
+
if (toolCalls === 0 || toolResults === 0) throw new Error(`${testCase.route}: tool-call round trip missing`);
|
|
101
|
+
if (turnErrors.length > 0) throw new Error(`${testCase.route}: error events: ${turnErrors.join(", ")}`);
|
|
102
|
+
|
|
103
|
+
const secondMarker = `${testCase.marker}_TURN2`;
|
|
104
|
+
await rpc("session.prompt", {
|
|
105
|
+
sessionId,
|
|
106
|
+
mode: "queue",
|
|
107
|
+
clientTimeZone: "UTC",
|
|
108
|
+
content: [
|
|
109
|
+
{ type: "text", text: `Second-turn replay test. Reply with exactly ${secondMarker} and no other text.` },
|
|
110
|
+
],
|
|
111
|
+
});
|
|
112
|
+
await waitForIdle(sessionId);
|
|
113
|
+
const secondHistory = await rpc("session.history", { sessionId, maxMessages: 200 });
|
|
114
|
+
const secondSerialized = JSON.stringify(secondHistory.events);
|
|
115
|
+
const secondEventTypes = [...new Set(secondHistory.events.map((entry) => entry.event.type))];
|
|
116
|
+
const secondTurnErrors = secondEventTypes.filter((type) => type.includes("error"));
|
|
117
|
+
if (!secondSerialized.includes(secondMarker))
|
|
118
|
+
throw new Error(`${testCase.route}: second-turn response marker missing`);
|
|
119
|
+
if (secondTurnErrors.length > 0)
|
|
120
|
+
throw new Error(`${testCase.route}: second-turn error events: ${secondTurnErrors.join(", ")}`);
|
|
121
|
+
console.log(`${testCase.route}/${testCase.model}: twoTurns=yes toolCalls=${toolCalls} toolResults=${toolResults}`);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
failure = error;
|
|
124
|
+
} finally {
|
|
125
|
+
if (selected) {
|
|
126
|
+
try {
|
|
127
|
+
await rpc("session.selectModel", { sessionId, ...restoreSelection });
|
|
128
|
+
} catch (error) {
|
|
129
|
+
failure ??= new Error(
|
|
130
|
+
`${testCase.route}: failed to restore default model: ${error instanceof Error ? error.message : String(error)}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
await rpc("workspace.archiveSession", { sessionId });
|
|
136
|
+
} catch (error) {
|
|
137
|
+
failure ??= new Error(
|
|
138
|
+
`${testCase.route}: failed to archive smoke session: ${error instanceof Error ? error.message : String(error)}`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (failure !== undefined) throw failure;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const testCase of cases) await runSmoke(testCase);
|
|
146
|
+
console.log("Deployed Codex/Kimi OAuth inference and tool-call smoke passed.");
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
const base = new URL(process.env.DSH_WEB_URL ?? "http://127.0.0.1:3080");
|
|
6
|
+
const origin = base.origin;
|
|
7
|
+
|
|
8
|
+
async function jsonFetch(path, init = {}) {
|
|
9
|
+
const response = await fetch(new URL(path, base), {
|
|
10
|
+
...init,
|
|
11
|
+
headers: {
|
|
12
|
+
accept: "application/json",
|
|
13
|
+
origin,
|
|
14
|
+
...init.headers,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
const value = await response.json().catch(() => undefined);
|
|
18
|
+
if (!response.ok) throw new Error(`${path}: HTTP ${response.status}`);
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function rpc(method, payload = {}) {
|
|
23
|
+
const rpcId = randomUUID();
|
|
24
|
+
const envelope = await jsonFetch(`/api/${method}`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: { "content-type": "application/json" },
|
|
27
|
+
body: JSON.stringify({ type: "client-request", rpcId, method, payload }),
|
|
28
|
+
});
|
|
29
|
+
if (envelope?.rpcId !== rpcId || envelope?.result?.ok !== true) {
|
|
30
|
+
throw new Error(`${method} failed: ${JSON.stringify(envelope?.result?.error ?? envelope)}`);
|
|
31
|
+
}
|
|
32
|
+
return envelope.result.value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const status = await jsonFetch("/plugins/dsh-grok-build/oauth/status");
|
|
36
|
+
const catalog = await rpc("llm.models");
|
|
37
|
+
const registered = await rpc("llm.providers");
|
|
38
|
+
const groups = new Map(catalog.groups.map((group) => [group.id, group]));
|
|
39
|
+
const routes = [
|
|
40
|
+
["grok", "grok-build"],
|
|
41
|
+
["codex", "codex-oauth"],
|
|
42
|
+
["kimi", "kimi-code-oauth"],
|
|
43
|
+
["claude", "claude-code-oauth"],
|
|
44
|
+
];
|
|
45
|
+
const failures = [];
|
|
46
|
+
const report = [];
|
|
47
|
+
for (const [slug, route] of routes) {
|
|
48
|
+
const authenticated = status?.providers?.[slug]?.status === "signed-in";
|
|
49
|
+
const group = groups.get(route);
|
|
50
|
+
if (authenticated && group === undefined) failures.push(`${route}: authenticated but absent from model catalog`);
|
|
51
|
+
if (!authenticated && group !== undefined)
|
|
52
|
+
failures.push(`${route}: unauthenticated but still advertises ${group.models.length} model(s)`);
|
|
53
|
+
if (group !== undefined && !/\(OAuth\)$/u.test(group.name))
|
|
54
|
+
failures.push(`${route}: provider name lacks (OAuth): ${group.name}`);
|
|
55
|
+
report.push(
|
|
56
|
+
`${route}: ${authenticated ? "authenticated" : "unauthenticated"} → ${group === undefined ? "hidden" : `${group.models.length} model(s), ${group.name}`}`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const providerIds = new Set(registered.providers.map((provider) => provider.provider));
|
|
61
|
+
const preservedRoutes = ["openai", "xai", "kimi-coding"];
|
|
62
|
+
for (const route of preservedRoutes) {
|
|
63
|
+
if (!providerIds.has(route)) failures.push(`${route}: legacy API-key route is no longer registered`);
|
|
64
|
+
}
|
|
65
|
+
report.push(`preserved API-key routes: ${preservedRoutes.filter((route) => providerIds.has(route)).join(", ")}`);
|
|
66
|
+
|
|
67
|
+
const agyExpectation = process.env.DSH_EXPECT_AGY_AUTH ?? "signed-out";
|
|
68
|
+
if (!["signed-out", "signed-in", "auto"].includes(agyExpectation)) {
|
|
69
|
+
throw new Error("DSH_EXPECT_AGY_AUTH must be signed-out, signed-in, or auto");
|
|
70
|
+
}
|
|
71
|
+
const agy = groups.get("agy");
|
|
72
|
+
if (agyExpectation === "signed-out" && agy !== undefined)
|
|
73
|
+
failures.push(`agy: expected signed-out but advertises ${agy.models.length} model(s)`);
|
|
74
|
+
if (agyExpectation === "signed-in" && agy === undefined)
|
|
75
|
+
failures.push("agy: expected signed-in but absent from model catalog");
|
|
76
|
+
if (agy !== undefined && agy.name !== "Google Antigravity (OAuth)")
|
|
77
|
+
failures.push(`agy: provider name lacks OAuth label: ${agy.name}`);
|
|
78
|
+
report.push(`agy: ${agy === undefined ? "hidden" : `${agy.models.length} model(s), ${agy.name}`}`);
|
|
79
|
+
|
|
80
|
+
console.log(report.join("\n"));
|
|
81
|
+
if (failures.length > 0) {
|
|
82
|
+
console.error("\nDeployment verification failed:");
|
|
83
|
+
for (const failure of failures) console.error(`- ${failure}`);
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
} else {
|
|
86
|
+
console.log("\nOAuth model catalog verification passed.");
|
|
87
|
+
}
|
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/** Coding-subscription adapter assembled from public dsh-llm-pi-ai extension points. */
|
|
2
|
+
|
|
3
|
+
import type { AttachmentStore } from "@deepseek-ai/dsh-attachment";
|
|
4
|
+
import type { RetryPolicyConfig } from "@deepseek-ai/dsh-llm";
|
|
5
|
+
import { type LlmAdapter, LlmError, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
6
|
+
import type { ResolvedPiAiProviderProfile } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
7
|
+
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
8
|
+
import type { AliasLlmRoutePolicy } from "./alias-adapter.ts";
|
|
9
|
+
import { AliasLlmAdapter } from "./alias-adapter.ts";
|
|
10
|
+
import { preferredGrokBuildModelFrom } from "./catalog.ts";
|
|
11
|
+
import { withCodexFastRouting } from "./codex-model-capabilities.ts";
|
|
12
|
+
import {
|
|
13
|
+
CLAUDE_CODE_OAUTH_ROUTE,
|
|
14
|
+
CLAUDE_PI_PROVIDER,
|
|
15
|
+
CODEX_OAUTH_FAST_ROUTE,
|
|
16
|
+
CODEX_OAUTH_ROUTE,
|
|
17
|
+
CODEX_PI_PROVIDER,
|
|
18
|
+
DEFAULT_GROK_BUILD_MODEL,
|
|
19
|
+
GROK_BUILD_ROUTE,
|
|
20
|
+
GROK_BUILD_STREAM_IDLE_TIMEOUT_MS,
|
|
21
|
+
KIMI_CODE_OAUTH_ROUTE,
|
|
22
|
+
KIMI_PI_PROVIDER,
|
|
23
|
+
XAI_PI_PROVIDER,
|
|
24
|
+
} from "./ids.ts";
|
|
25
|
+
import type { OAuthProviderSession } from "./oauth-session.ts";
|
|
26
|
+
import { grokBuildBaselineModels, grokBuildFingerprintHeaders } from "./provider.ts";
|
|
27
|
+
import { safeMessage } from "./redact.ts";
|
|
28
|
+
import type { GrokBuildSession } from "./session.ts";
|
|
29
|
+
|
|
30
|
+
/** Prefer grok-4.6 when the current (live or baseline) list has it. */
|
|
31
|
+
export function preferredGrokBuildModel(models: readonly { id: string }[] = grokBuildBaselineModels()): string {
|
|
32
|
+
return preferredGrokBuildModelFrom(models.length === 0 ? [{ id: DEFAULT_GROK_BUILD_MODEL }] : models);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function missingCredential(name: string): never {
|
|
36
|
+
throw new LlmError(
|
|
37
|
+
`${name} is not signed in. Open Settings → Coding OAuth and sign in with your subscription.`,
|
|
38
|
+
"MISSING_CREDENTIAL",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Minimum remaining validity demanded of an exported OAuth access token.
|
|
44
|
+
* pi-ai 0.84+ already refreshes five minutes before the stored expiry; this
|
|
45
|
+
* explicit floor documents the plugin contract and hard-fails a refresh that
|
|
46
|
+
* returns an even-shorter-lived token instead of handing it to a request.
|
|
47
|
+
*/
|
|
48
|
+
const MIN_OAUTH_VALIDITY_MS = 60_000;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Provider retry policy for the coding-subscription routes. The harness
|
|
52
|
+
* default retryable set (EMPTY_RESPONSE/RATE_LIMIT/SERVER/TIMEOUT/TRANSPORT)
|
|
53
|
+
* deliberately excludes AUTH, so an upstream 401 — e.g. an access token the
|
|
54
|
+
* server revoked before its local expiry — used to kill the turn outright.
|
|
55
|
+
* AUTH is added here because {@link AliasLlmAdapter} invalidates the stored
|
|
56
|
+
* credential on every AUTH finish, so the retried step refreshes first and
|
|
57
|
+
* does not repeat the same rejected token. Quota exhaustion stays outside the
|
|
58
|
+
* set: retrying a billing-limit 403 cannot succeed and only delays the real
|
|
59
|
+
* message. Genuine credential death is converted to MISSING_CREDENTIAL (not
|
|
60
|
+
* retryable) by the resolver below, so it cannot loop either.
|
|
61
|
+
*/
|
|
62
|
+
const CODING_OAUTH_RETRY_POLICY = {
|
|
63
|
+
mode: "normal" as const,
|
|
64
|
+
maxRetries: 2,
|
|
65
|
+
retryableCodes: ["EMPTY_RESPONSE", "RATE_LIMIT", "SERVER", "TIMEOUT", "TRANSPORT", "AUTH"],
|
|
66
|
+
backoff: { initialDelayMs: 500, maxDelayMs: 10_000, jitterRatio: 0.1 },
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function profile(
|
|
70
|
+
provider: string,
|
|
71
|
+
displayName: string,
|
|
72
|
+
piProvider: ResolvedPiAiProviderProfile["piProvider"],
|
|
73
|
+
retryPolicy?: RetryPolicyConfig | undefined,
|
|
74
|
+
headers?: Record<string, string> | undefined,
|
|
75
|
+
): ResolvedPiAiProviderProfile {
|
|
76
|
+
return {
|
|
77
|
+
provider,
|
|
78
|
+
displayName,
|
|
79
|
+
streamIdleTimeoutMs: GROK_BUILD_STREAM_IDLE_TIMEOUT_MS,
|
|
80
|
+
retryPolicy: resolveRetryPolicy(
|
|
81
|
+
retryPolicy ?? CODING_OAUTH_RETRY_POLICY,
|
|
82
|
+
"dsh-coding-subscription-oauth retryPolicy",
|
|
83
|
+
),
|
|
84
|
+
configuredMaxTokens: new Map(),
|
|
85
|
+
...(headers === undefined ? {} : { headers }),
|
|
86
|
+
piProvider,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Existing Grok-only constructor retained for public API compatibility. */
|
|
91
|
+
export function createGrokBuildAdapter(
|
|
92
|
+
session: GrokBuildSession,
|
|
93
|
+
resolveAttachments: () => AttachmentStore | undefined,
|
|
94
|
+
): PiAiAdapter {
|
|
95
|
+
return new PiAiAdapter({
|
|
96
|
+
profiles: () =>
|
|
97
|
+
new Map<string, ResolvedPiAiProviderProfile>([
|
|
98
|
+
[
|
|
99
|
+
GROK_BUILD_ROUTE,
|
|
100
|
+
profile(GROK_BUILD_ROUTE, "xAI Grok Build", session.provider(), undefined, grokBuildFingerprintHeaders()),
|
|
101
|
+
],
|
|
102
|
+
]),
|
|
103
|
+
resolveApiKey: async () =>
|
|
104
|
+
resolveOAuthToken("Grok Build", async () => {
|
|
105
|
+
const auth = await session.models.getAuth(XAI_PI_PROVIDER, { minOAuthValidityMs: MIN_OAUTH_VALIDITY_MS });
|
|
106
|
+
return auth?.auth.apiKey;
|
|
107
|
+
}),
|
|
108
|
+
resolveAttachments,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Opt-in Codex Fast wiring; ordinary `codex-oauth` is unchanged when this is omitted. */
|
|
113
|
+
export interface CodingOAuthAdapterOptions {
|
|
114
|
+
retryPolicy?: RetryPolicyConfig;
|
|
115
|
+
codexFast?: { isEligible(modelId: string): boolean };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isRetryPolicyConfig(value: object): value is RetryPolicyConfig {
|
|
119
|
+
return "mode" in value;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function splitCodingOAuthAdapterArgs(
|
|
123
|
+
fourth?: RetryPolicyConfig | CodingOAuthAdapterOptions,
|
|
124
|
+
fifth?: CodingOAuthAdapterOptions,
|
|
125
|
+
): CodingOAuthAdapterOptions {
|
|
126
|
+
if (fifth !== undefined) {
|
|
127
|
+
return {
|
|
128
|
+
...(fourth !== undefined && isRetryPolicyConfig(fourth) ? { retryPolicy: fourth } : {}),
|
|
129
|
+
...fifth,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (fourth === undefined) return {};
|
|
133
|
+
if (isRetryPolicyConfig(fourth)) return { retryPolicy: fourth };
|
|
134
|
+
return {
|
|
135
|
+
...(fourth.retryPolicy === undefined ? {} : { retryPolicy: fourth.retryPolicy }),
|
|
136
|
+
...(fourth.codexFast === undefined ? {} : { codexFast: fourth.codexFast }),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Create the four-route OAuth adapter while preserving each pi-ai native id. */
|
|
141
|
+
export function createCodingOAuthAdapter(
|
|
142
|
+
grok: GrokBuildSession,
|
|
143
|
+
subscriptions: readonly OAuthProviderSession[],
|
|
144
|
+
resolveAttachments: () => AttachmentStore | undefined,
|
|
145
|
+
retryPolicy?: RetryPolicyConfig,
|
|
146
|
+
options?: CodingOAuthAdapterOptions,
|
|
147
|
+
): LlmAdapter;
|
|
148
|
+
export function createCodingOAuthAdapter(
|
|
149
|
+
grok: GrokBuildSession,
|
|
150
|
+
subscriptions: readonly OAuthProviderSession[],
|
|
151
|
+
resolveAttachments: () => AttachmentStore | undefined,
|
|
152
|
+
options?: CodingOAuthAdapterOptions,
|
|
153
|
+
): LlmAdapter;
|
|
154
|
+
export function createCodingOAuthAdapter(
|
|
155
|
+
grok: GrokBuildSession,
|
|
156
|
+
subscriptions: readonly OAuthProviderSession[],
|
|
157
|
+
resolveAttachments: () => AttachmentStore | undefined,
|
|
158
|
+
retryPolicyOrOptions?: RetryPolicyConfig | CodingOAuthAdapterOptions,
|
|
159
|
+
options?: CodingOAuthAdapterOptions,
|
|
160
|
+
): LlmAdapter {
|
|
161
|
+
const { retryPolicy, codexFast } = splitCodingOAuthAdapterArgs(retryPolicyOrOptions, options);
|
|
162
|
+
const byNativeId = new Map(subscriptions.map((session) => [session.definition.nativeProviderId, session]));
|
|
163
|
+
const codexSession = byNativeId.get(CODEX_PI_PROVIDER);
|
|
164
|
+
const aliases = new Map<string, string>([
|
|
165
|
+
[GROK_BUILD_ROUTE, GROK_BUILD_ROUTE],
|
|
166
|
+
[CODEX_OAUTH_ROUTE, CODEX_PI_PROVIDER],
|
|
167
|
+
[KIMI_CODE_OAUTH_ROUTE, KIMI_PI_PROVIDER],
|
|
168
|
+
[CLAUDE_CODE_OAUTH_ROUTE, CLAUDE_PI_PROVIDER],
|
|
169
|
+
]);
|
|
170
|
+
if (codexFast !== undefined && codexSession !== undefined) {
|
|
171
|
+
aliases.set(CODEX_OAUTH_FAST_ROUTE, CODEX_OAUTH_FAST_ROUTE);
|
|
172
|
+
}
|
|
173
|
+
const policies = new Map<string, AliasLlmRoutePolicy>([
|
|
174
|
+
[
|
|
175
|
+
GROK_BUILD_ROUTE,
|
|
176
|
+
{
|
|
177
|
+
displayName: "xAI Grok Build (OAuth)",
|
|
178
|
+
isAuthenticated: async () => (await grok.store.read(XAI_PI_PROVIDER))?.type === "oauth",
|
|
179
|
+
onAuthFailure: () => grok.invalidateAccessToken(),
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
]);
|
|
183
|
+
for (const session of subscriptions) {
|
|
184
|
+
policies.set(session.definition.route, {
|
|
185
|
+
displayName: `${session.definition.displayName.replace(/\s*\([^)]*\)$/u, "")} (OAuth)`,
|
|
186
|
+
isAuthenticated: async () => (await session.status()).authenticated,
|
|
187
|
+
onAuthFailure: () => session.invalidateAccessToken(),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (codexFast !== undefined && codexSession !== undefined) {
|
|
191
|
+
policies.set(CODEX_OAUTH_FAST_ROUTE, {
|
|
192
|
+
displayName: "OpenAI Codex Fast requested (OAuth)",
|
|
193
|
+
isAuthenticated: async () => (await codexSession.status()).authenticated,
|
|
194
|
+
includeModel: (modelId) => codexFast.isEligible(modelId),
|
|
195
|
+
onAuthFailure: () => codexSession.invalidateAccessToken(),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const inner = new PiAiAdapter({
|
|
200
|
+
profiles: () => {
|
|
201
|
+
const profiles = new Map<string, ResolvedPiAiProviderProfile>();
|
|
202
|
+
profiles.set(
|
|
203
|
+
GROK_BUILD_ROUTE,
|
|
204
|
+
profile(GROK_BUILD_ROUTE, "xAI Grok Build", grok.provider(), retryPolicy, grokBuildFingerprintHeaders()),
|
|
205
|
+
);
|
|
206
|
+
for (const session of subscriptions) {
|
|
207
|
+
profiles.set(
|
|
208
|
+
session.definition.nativeProviderId,
|
|
209
|
+
profile(session.definition.nativeProviderId, session.definition.displayName, session.provider(), retryPolicy),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (codexFast !== undefined && codexSession !== undefined) {
|
|
213
|
+
const wrapped = withCodexFastRouting(codexSession.provider(), {
|
|
214
|
+
isEligible: (modelId) => codexFast.isEligible(modelId),
|
|
215
|
+
profileProviderId: CODEX_OAUTH_FAST_ROUTE,
|
|
216
|
+
nativeProviderId: CODEX_PI_PROVIDER,
|
|
217
|
+
});
|
|
218
|
+
// Models.streamSimple dispatches on model.provider. Advertise the Fast
|
|
219
|
+
// profile id on the catalog so the wrapper runs, then restore native
|
|
220
|
+
// identity inside withCodexFastRouting before the wire call.
|
|
221
|
+
const fastProvider = {
|
|
222
|
+
...wrapped,
|
|
223
|
+
getModels: () =>
|
|
224
|
+
wrapped
|
|
225
|
+
.getModels()
|
|
226
|
+
.map((model) =>
|
|
227
|
+
model.provider === CODEX_OAUTH_FAST_ROUTE ? model : { ...model, provider: CODEX_OAUTH_FAST_ROUTE },
|
|
228
|
+
),
|
|
229
|
+
};
|
|
230
|
+
profiles.set(
|
|
231
|
+
CODEX_OAUTH_FAST_ROUTE,
|
|
232
|
+
profile(
|
|
233
|
+
CODEX_OAUTH_FAST_ROUTE,
|
|
234
|
+
"OpenAI Codex Fast requested",
|
|
235
|
+
fastProvider as unknown as ResolvedPiAiProviderProfile["piProvider"],
|
|
236
|
+
retryPolicy,
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
return profiles;
|
|
241
|
+
},
|
|
242
|
+
resolveApiKey: async (provider) => {
|
|
243
|
+
if (provider === GROK_BUILD_ROUTE) {
|
|
244
|
+
return resolveOAuthToken("Grok Build", async () => {
|
|
245
|
+
const auth = await grok.models.getAuth(XAI_PI_PROVIDER, { minOAuthValidityMs: MIN_OAUTH_VALIDITY_MS });
|
|
246
|
+
return auth?.auth.apiKey;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
const session =
|
|
250
|
+
provider === CODEX_OAUTH_FAST_ROUTE ? byNativeId.get(CODEX_PI_PROVIDER) : byNativeId.get(provider);
|
|
251
|
+
if (session === undefined) throw new LlmError(`Unknown OAuth provider "${provider}"`, "NO_ADAPTER");
|
|
252
|
+
return resolveOAuthToken(session.definition.displayName, () => session.resolveAccessToken());
|
|
253
|
+
},
|
|
254
|
+
resolveAttachments,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
return new AliasLlmAdapter(inner, aliases, policies);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Resolve an OAuth access token for one route, translating a failed refresh
|
|
262
|
+
* (revoked refresh token, dead grant) into MISSING_CREDENTIAL so the failure
|
|
263
|
+
* is not retried and the user is told to sign in again rather than shown a
|
|
264
|
+
* bare upstream 401.
|
|
265
|
+
*/
|
|
266
|
+
async function resolveOAuthToken(
|
|
267
|
+
displayName: string,
|
|
268
|
+
getAccessToken: () => Promise<string | undefined>,
|
|
269
|
+
): Promise<string> {
|
|
270
|
+
let accessToken: string | undefined;
|
|
271
|
+
try {
|
|
272
|
+
accessToken = await getAccessToken();
|
|
273
|
+
} catch (error) {
|
|
274
|
+
throw new LlmError(
|
|
275
|
+
`${displayName} could not refresh its sign-in (${safeMessage(error)}).` +
|
|
276
|
+
" Open Settings → Coding OAuth and sign in again.",
|
|
277
|
+
"MISSING_CREDENTIAL",
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
if (accessToken === undefined || accessToken.length === 0) return missingCredential(displayName);
|
|
281
|
+
return accessToken;
|
|
282
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness route aliases over a native-id PiAiAdapter.
|
|
3
|
+
* @module dsh-coding-subscription-oauth/alias-adapter
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
GenerateOptions,
|
|
8
|
+
LlmModelInfo,
|
|
9
|
+
LlmProviderInfo,
|
|
10
|
+
LlmResolvedModelInfo,
|
|
11
|
+
ResolvedRetryPolicy,
|
|
12
|
+
StreamChunk,
|
|
13
|
+
} from "@deepseek-ai/dsh-llm";
|
|
14
|
+
import { LlmAdapter, LlmError } from "@deepseek-ai/dsh-llm";
|
|
15
|
+
import { remapAuthFailureIfContextOverflow } from "./kimi-errors.ts";
|
|
16
|
+
|
|
17
|
+
export interface AliasLlmRoutePolicy {
|
|
18
|
+
/** User-facing provider name shown above models in the model selector. */
|
|
19
|
+
displayName?: string;
|
|
20
|
+
/** Return false to hide every model for this route from discovery. */
|
|
21
|
+
isAuthenticated?: () => Promise<boolean>;
|
|
22
|
+
/**
|
|
23
|
+
* After authentication, keep only models this predicate accepts.
|
|
24
|
+
* Used by opt-in routes (e.g. Codex Fast) so ineligible ids stay hidden
|
|
25
|
+
* without hiding the whole authenticated catalog.
|
|
26
|
+
*/
|
|
27
|
+
includeModel?: (modelId: string) => boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Called once when a stream for this route finishes with an AUTH failure
|
|
30
|
+
* (upstream rejected a locally-valid token). Implementations backdate the
|
|
31
|
+
* stored credential's expiry so the retried step refreshes before reuse.
|
|
32
|
+
* Awaiting it here keeps invalidation ordered before the retry executor
|
|
33
|
+
* reruns the step; failures are swallowed so the original AUTH surfaces.
|
|
34
|
+
*/
|
|
35
|
+
onAuthFailure?: () => Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function routePiAiReplayState(value: unknown, route: string): unknown {
|
|
39
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
|
|
40
|
+
const state = value as Record<string, unknown>;
|
|
41
|
+
if (state["kind"] !== "pi-ai" || state["provider"] === route) return value;
|
|
42
|
+
return { ...state, provider: route };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeReplayForRoute(
|
|
46
|
+
message: GenerateOptions["messages"][number],
|
|
47
|
+
route: string,
|
|
48
|
+
): GenerateOptions["messages"][number] {
|
|
49
|
+
if (message.role !== "assistant" || message.source.kind !== "model") return message;
|
|
50
|
+
if (message.source.provider !== route) {
|
|
51
|
+
if (message.source.replayState === undefined) return message;
|
|
52
|
+
const { replayState: _foreignReplay, ...source } = message.source;
|
|
53
|
+
return { ...message, source };
|
|
54
|
+
}
|
|
55
|
+
const replayState = routePiAiReplayState(message.source.replayState, route);
|
|
56
|
+
if (replayState === message.source.replayState) return message;
|
|
57
|
+
return { ...message, source: { ...message.source, replayState } };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Keeps pi-ai model.provider identities native while exposing collision-free
|
|
62
|
+
* Harness route names. Every public operation translates exactly once.
|
|
63
|
+
*/
|
|
64
|
+
export class AliasLlmAdapter extends LlmAdapter {
|
|
65
|
+
constructor(
|
|
66
|
+
private readonly inner: LlmAdapter,
|
|
67
|
+
private readonly aliases: ReadonlyMap<string, string>,
|
|
68
|
+
private readonly policies: ReadonlyMap<string, AliasLlmRoutePolicy> = new Map(),
|
|
69
|
+
) {
|
|
70
|
+
super();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private nativeProvider(route: string): string {
|
|
74
|
+
const provider = this.aliases.get(route);
|
|
75
|
+
if (provider === undefined) throw new LlmError(`OAuth adapter does not own provider "${route}"`, "NO_ADAPTER");
|
|
76
|
+
return provider;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
providerInfo(provider: string): LlmProviderInfo {
|
|
80
|
+
const native = this.nativeProvider(provider);
|
|
81
|
+
const info = this.inner.providerInfo(native);
|
|
82
|
+
const displayName = this.policies.get(provider)?.displayName;
|
|
83
|
+
return { ...info, id: provider, ...(displayName === undefined ? {} : { name: displayName }) };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
providerRetryPolicy(provider: string): ResolvedRetryPolicy | undefined {
|
|
87
|
+
return this.inner.providerRetryPolicy(this.nativeProvider(provider));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async listModels(provider: string): Promise<readonly LlmModelInfo[]> {
|
|
91
|
+
const native = this.nativeProvider(provider);
|
|
92
|
+
const isAuthenticated = this.policies.get(provider)?.isAuthenticated;
|
|
93
|
+
if (isAuthenticated !== undefined) {
|
|
94
|
+
try {
|
|
95
|
+
if (!(await isAuthenticated())) return [];
|
|
96
|
+
} catch {
|
|
97
|
+
// A corrupt/unreadable credential must hide models instead of breaking
|
|
98
|
+
// the entire model picker or presenting a route that cannot be used.
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const listed = (await this.inner.listModels(native)).map((model) => ({ ...model, provider }));
|
|
103
|
+
const includeModel = this.policies.get(provider)?.includeModel;
|
|
104
|
+
return includeModel === undefined ? listed : listed.filter((model) => includeModel(model.id));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async resolveModel(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo> {
|
|
108
|
+
const native = this.nativeProvider(provider);
|
|
109
|
+
return { ...(await this.inner.resolveModel(native, model, signal)), provider };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
|
113
|
+
const route = options.provider;
|
|
114
|
+
const native = this.nativeProvider(route);
|
|
115
|
+
const messages = options.messages.map((message) => normalizeReplayForRoute(message, route));
|
|
116
|
+
let authFailureNotified = false;
|
|
117
|
+
for await (const raw of this.inner.stream({ ...options, provider: native, messages })) {
|
|
118
|
+
const chunk =
|
|
119
|
+
raw.type === "finish" && raw.reason.kind === "error"
|
|
120
|
+
? {
|
|
121
|
+
...raw,
|
|
122
|
+
reason: {
|
|
123
|
+
...raw.reason,
|
|
124
|
+
failure: remapAuthFailureIfContextOverflow(raw.reason.failure),
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
: raw;
|
|
128
|
+
if (
|
|
129
|
+
!authFailureNotified &&
|
|
130
|
+
chunk.type === "finish" &&
|
|
131
|
+
chunk.reason.kind === "error" &&
|
|
132
|
+
chunk.reason.failure.code === "AUTH"
|
|
133
|
+
) {
|
|
134
|
+
authFailureNotified = true;
|
|
135
|
+
const onAuthFailure = this.policies.get(route)?.onAuthFailure;
|
|
136
|
+
if (onAuthFailure !== undefined) {
|
|
137
|
+
try {
|
|
138
|
+
await onAuthFailure();
|
|
139
|
+
} catch {
|
|
140
|
+
// Invalidation is best-effort: the original AUTH failure must
|
|
141
|
+
// surface unchanged even when the credential store is unreadable.
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (chunk.type === "finish" && chunk.replayState !== undefined) {
|
|
146
|
+
yield { ...chunk, replayState: routePiAiReplayState(chunk.replayState, route) };
|
|
147
|
+
} else {
|
|
148
|
+
yield chunk;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|