@goodandready/dsh-opencode-provider 0.5.1 → 0.5.3
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 +15 -0
- package/README.md +307 -301
- package/README.ru.md +208 -202
- package/README.zh.md +184 -177
- package/lib/client.js +314 -6
- package/lib/index.d.ts +31 -1
- package/lib/index.js +188 -32
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -17,7 +17,7 @@ const DEFAULT_GO_API_BASE = "https://opencode.ai/inference/go";
|
|
|
17
17
|
const DEFAULT_GO_ZEN_BASE = "https://opencode.ai/zen/go/v1";
|
|
18
18
|
const DEFAULT_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
|
|
19
19
|
const DEFAULT_AUTH_ENTRY = "opencode-go";
|
|
20
|
-
const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.
|
|
20
|
+
const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.3";
|
|
21
21
|
const DEFAULT_CONTEXT_WINDOW = 262144;
|
|
22
22
|
const DEFAULT_MAX_TOKENS = 32768;
|
|
23
23
|
const DEFAULT_GENERATE_MAX_TOKENS = 32768;
|
|
@@ -56,6 +56,8 @@ const Config = z.object({
|
|
|
56
56
|
pattern: z.string(),
|
|
57
57
|
account: z.string()
|
|
58
58
|
})).default([])),
|
|
59
|
+
quotaFallbackModel: markVolatile(z.string().default("")),
|
|
60
|
+
stickySession: markVolatile(z.boolean().default(true)),
|
|
59
61
|
activeAccount: markVolatile(z.string().default("")),
|
|
60
62
|
showSidebarQuota: markVolatile(z.boolean().default(false)),
|
|
61
63
|
userAgent: markVolatile(z.string().default(DEFAULT_USER_AGENT))
|
|
@@ -141,6 +143,8 @@ function resolveConfig(config) {
|
|
|
141
143
|
...typeof config.maxOutputTokens === "number" && Number.isInteger(config.maxOutputTokens) && config.maxOutputTokens >= 0 ? { maxOutputTokens: config.maxOutputTokens } : {},
|
|
142
144
|
accounts: Array.isArray(config.accounts) ? config.accounts : [],
|
|
143
145
|
modelAccountRules: modelAccountRuleList(config.modelAccountRules),
|
|
146
|
+
quotaFallbackModel: typeof config.quotaFallbackModel === "string" ? config.quotaFallbackModel.trim() : "",
|
|
147
|
+
stickySession: config.stickySession !== false,
|
|
144
148
|
activeAccount: typeof config.activeAccount === "string" ? config.activeAccount : "",
|
|
145
149
|
showSidebarQuota: config.showSidebarQuota === true,
|
|
146
150
|
userAgent: typeof config.userAgent === "string" && config.userAgent !== "" ? config.userAgent : DEFAULT_USER_AGENT
|
|
@@ -1242,6 +1246,17 @@ function offloadPlaceholder(ref) {
|
|
|
1242
1246
|
return `[image omitted: ${ref.name ?? ref.attachmentId}]`;
|
|
1243
1247
|
}
|
|
1244
1248
|
//#endregion
|
|
1249
|
+
//#region src/files.ts
|
|
1250
|
+
/**
|
|
1251
|
+
* Model-facing handle for a file when the adapter receives a raw FileBlock.
|
|
1252
|
+
* The harness normally projects files to text with an execution-world path
|
|
1253
|
+
* before dispatch; this is the safe fallback for direct adapter calls.
|
|
1254
|
+
*/
|
|
1255
|
+
function filePlaceholder(ref) {
|
|
1256
|
+
const digest = String(ref.attachmentId).slice(7, 15);
|
|
1257
|
+
return `[File ${JSON.stringify(ref.name)} (${ref.bytes} bytes, sha256:${digest}) was uploaded, but the current execution environment cannot access a readable path. Report that limitation if its contents are needed; do not claim to have read it.]`;
|
|
1258
|
+
}
|
|
1259
|
+
//#endregion
|
|
1245
1260
|
//#region src/tool-schema.ts
|
|
1246
1261
|
/**
|
|
1247
1262
|
* Normalize a tool's parameters schema to the object root the gateway
|
|
@@ -1354,7 +1369,7 @@ function textOnly$3(content) {
|
|
|
1354
1369
|
const parts = [];
|
|
1355
1370
|
for (const block of content) if (block.type === "text") parts.push(block.text);
|
|
1356
1371
|
else if (block.type === "image") parts.push(offloadPlaceholder(block.attachment));
|
|
1357
|
-
else if (block.type === "file") parts.push(
|
|
1372
|
+
else if (block.type === "file") parts.push(filePlaceholder(block.attachment));
|
|
1358
1373
|
return parts.join("");
|
|
1359
1374
|
}
|
|
1360
1375
|
function userContent(content, images) {
|
|
@@ -1386,7 +1401,10 @@ function userContent(content, images) {
|
|
|
1386
1401
|
type: "image_url",
|
|
1387
1402
|
image_url: { url: image.dataUrl }
|
|
1388
1403
|
});
|
|
1389
|
-
} else if (block.type === "file")
|
|
1404
|
+
} else if (block.type === "file") parts.push({
|
|
1405
|
+
type: "text",
|
|
1406
|
+
text: filePlaceholder(block.attachment)
|
|
1407
|
+
});
|
|
1390
1408
|
if (!hasImage) return parts.map((part) => part.type === "text" ? part.text : "").join("");
|
|
1391
1409
|
return parts;
|
|
1392
1410
|
}
|
|
@@ -1506,22 +1524,26 @@ function requestLogPath() {
|
|
|
1506
1524
|
return join(home, "storages", "dsh-opencode", "requests.jsonl");
|
|
1507
1525
|
}
|
|
1508
1526
|
let queue = Promise.resolve();
|
|
1509
|
-
let
|
|
1527
|
+
let readyDirectory = "";
|
|
1510
1528
|
let appends = 0;
|
|
1511
|
-
function recordRequest(provider, model, at = Date.now(), purpose) {
|
|
1529
|
+
function recordRequest(provider, model, at = Date.now(), purpose, account, reason) {
|
|
1512
1530
|
const line = `${JSON.stringify({
|
|
1513
1531
|
at,
|
|
1514
1532
|
provider,
|
|
1515
1533
|
model,
|
|
1516
|
-
...purpose === void 0 ? {} : { purpose }
|
|
1534
|
+
...purpose === void 0 ? {} : { purpose },
|
|
1535
|
+
...account === void 0 ? {} : { account },
|
|
1536
|
+
...reason === void 0 ? {} : { reason }
|
|
1517
1537
|
})}\n`;
|
|
1518
1538
|
queue = queue.then(async () => {
|
|
1519
1539
|
try {
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1540
|
+
const path = requestLogPath();
|
|
1541
|
+
const directory = dirname(path);
|
|
1542
|
+
if (readyDirectory !== directory) {
|
|
1543
|
+
await mkdir(directory, { recursive: true });
|
|
1544
|
+
readyDirectory = directory;
|
|
1523
1545
|
}
|
|
1524
|
-
await appendFile(
|
|
1546
|
+
await appendFile(path, line, "utf8");
|
|
1525
1547
|
appends += 1;
|
|
1526
1548
|
if (appends % COMPACT_EVERY === 0) await compactRequestLog();
|
|
1527
1549
|
} catch {}
|
|
@@ -1542,6 +1564,7 @@ async function readRequestHeatmap(days, now = Date.now()) {
|
|
|
1542
1564
|
const hours = Array.from({ length: 24 }, () => 0);
|
|
1543
1565
|
const grid = Array.from({ length: 7 }, () => Array.from({ length: 24 }, () => 0));
|
|
1544
1566
|
const models = /* @__PURE__ */ new Map();
|
|
1567
|
+
const accounts = /* @__PURE__ */ new Map();
|
|
1545
1568
|
const seen = /* @__PURE__ */ new Set();
|
|
1546
1569
|
let total = 0;
|
|
1547
1570
|
let firstAt = null;
|
|
@@ -1557,7 +1580,8 @@ async function readRequestHeatmap(days, now = Date.now()) {
|
|
|
1557
1580
|
grid,
|
|
1558
1581
|
firstAt,
|
|
1559
1582
|
lastAt,
|
|
1560
|
-
models: []
|
|
1583
|
+
models: [],
|
|
1584
|
+
accounts: []
|
|
1561
1585
|
};
|
|
1562
1586
|
}
|
|
1563
1587
|
for (const line of text.split("\n")) {
|
|
@@ -1575,7 +1599,8 @@ async function readRequestHeatmap(days, now = Date.now()) {
|
|
|
1575
1599
|
if (purpose === "session-title" || purpose === "compaction") continue;
|
|
1576
1600
|
const provider = typeof parsed.provider === "string" ? parsed.provider : "";
|
|
1577
1601
|
const model = typeof parsed.model === "string" ? parsed.model : "";
|
|
1578
|
-
const
|
|
1602
|
+
const account = typeof parsed.account === "string" && parsed.account !== "" ? parsed.account : "unknown";
|
|
1603
|
+
const key = `${at}|${provider}|${model}|${account}`;
|
|
1579
1604
|
if (seen.has(key)) continue;
|
|
1580
1605
|
seen.add(key);
|
|
1581
1606
|
const date = new Date(at);
|
|
@@ -1602,6 +1627,20 @@ async function readRequestHeatmap(days, now = Date.now()) {
|
|
|
1602
1627
|
const modelRow = entry.grid[day];
|
|
1603
1628
|
if (modelRow !== void 0) modelRow[hour] = (modelRow[hour] ?? 0) + 1;
|
|
1604
1629
|
}
|
|
1630
|
+
let accountEntry = accounts.get(account);
|
|
1631
|
+
if (accountEntry === void 0) {
|
|
1632
|
+
accountEntry = {
|
|
1633
|
+
account,
|
|
1634
|
+
total: 0,
|
|
1635
|
+
hours: Array.from({ length: 24 }, () => 0),
|
|
1636
|
+
grid: Array.from({ length: 7 }, () => Array.from({ length: 24 }, () => 0))
|
|
1637
|
+
};
|
|
1638
|
+
accounts.set(account, accountEntry);
|
|
1639
|
+
}
|
|
1640
|
+
accountEntry.total += 1;
|
|
1641
|
+
accountEntry.hours[hour] = (accountEntry.hours[hour] ?? 0) + 1;
|
|
1642
|
+
const accountRow = accountEntry.grid[day];
|
|
1643
|
+
if (accountRow !== void 0) accountRow[hour] = (accountRow[hour] ?? 0) + 1;
|
|
1605
1644
|
total += 1;
|
|
1606
1645
|
if (firstAt === null || at < firstAt) firstAt = at;
|
|
1607
1646
|
if (lastAt === null || at > lastAt) lastAt = at;
|
|
@@ -1613,7 +1652,8 @@ async function readRequestHeatmap(days, now = Date.now()) {
|
|
|
1613
1652
|
grid,
|
|
1614
1653
|
firstAt,
|
|
1615
1654
|
lastAt,
|
|
1616
|
-
models: [...models.values()].sort((a, b) => b.total - a.total || a.model.localeCompare(b.model))
|
|
1655
|
+
models: [...models.values()].sort((a, b) => b.total - a.total || a.model.localeCompare(b.model)),
|
|
1656
|
+
accounts: [...accounts.values()].sort((a, b) => b.total - a.total || a.account.localeCompare(b.account))
|
|
1617
1657
|
};
|
|
1618
1658
|
}
|
|
1619
1659
|
//#endregion
|
|
@@ -1775,7 +1815,7 @@ function textOnly$2(content) {
|
|
|
1775
1815
|
const parts = [];
|
|
1776
1816
|
for (const block of content) if (block.type === "text") parts.push(block.text);
|
|
1777
1817
|
else if (block.type === "image") parts.push(offloadPlaceholder(block.attachment));
|
|
1778
|
-
else if (block.type === "file") parts.push(
|
|
1818
|
+
else if (block.type === "file") parts.push(filePlaceholder(block.attachment));
|
|
1779
1819
|
return parts.join("");
|
|
1780
1820
|
}
|
|
1781
1821
|
function contentParts(content, images) {
|
|
@@ -1810,7 +1850,10 @@ function contentParts(content, images) {
|
|
|
1810
1850
|
data: image.base64
|
|
1811
1851
|
}
|
|
1812
1852
|
});
|
|
1813
|
-
} else if (block.type === "file")
|
|
1853
|
+
} else if (block.type === "file") parts.push({
|
|
1854
|
+
type: "text",
|
|
1855
|
+
text: filePlaceholder(block.attachment)
|
|
1856
|
+
});
|
|
1814
1857
|
return parts.length === 0 ? [{
|
|
1815
1858
|
type: "text",
|
|
1816
1859
|
text: ""
|
|
@@ -2151,7 +2194,7 @@ function textOnly$1(content) {
|
|
|
2151
2194
|
const parts = [];
|
|
2152
2195
|
for (const block of content) if (block.type === "text") parts.push(block.text);
|
|
2153
2196
|
else if (block.type === "image") parts.push(offloadPlaceholder(block.attachment));
|
|
2154
|
-
else if (block.type === "file") parts.push(
|
|
2197
|
+
else if (block.type === "file") parts.push(filePlaceholder(block.attachment));
|
|
2155
2198
|
return parts.join("");
|
|
2156
2199
|
}
|
|
2157
2200
|
function imageParts(block, images) {
|
|
@@ -2171,7 +2214,7 @@ function userParts$1(content, images) {
|
|
|
2171
2214
|
for (const block of content) if (block.type === "text") {
|
|
2172
2215
|
if (block.text !== "") parts.push({ text: block.text });
|
|
2173
2216
|
} else if (block.type === "image") parts.push(...imageParts(block, images));
|
|
2174
|
-
else if (block.type === "file")
|
|
2217
|
+
else if (block.type === "file") parts.push({ text: filePlaceholder(block.attachment) });
|
|
2175
2218
|
return parts.length === 0 ? [{ text: "" }] : parts;
|
|
2176
2219
|
}
|
|
2177
2220
|
function replayMeta(message) {
|
|
@@ -2691,7 +2734,7 @@ function textOnly(content) {
|
|
|
2691
2734
|
const parts = [];
|
|
2692
2735
|
for (const block of content) if (block.type === "text") parts.push(block.text);
|
|
2693
2736
|
else if (block.type === "image") parts.push(offloadPlaceholder(block.attachment));
|
|
2694
|
-
else if (block.type === "file") parts.push(
|
|
2737
|
+
else if (block.type === "file") parts.push(filePlaceholder(block.attachment));
|
|
2695
2738
|
return parts.join("");
|
|
2696
2739
|
}
|
|
2697
2740
|
function userParts(content, images) {
|
|
@@ -2722,7 +2765,10 @@ function userParts(content, images) {
|
|
|
2722
2765
|
type: "input_image",
|
|
2723
2766
|
image_url: image.dataUrl
|
|
2724
2767
|
});
|
|
2725
|
-
} else if (block.type === "file")
|
|
2768
|
+
} else if (block.type === "file") parts.push({
|
|
2769
|
+
type: "input_text",
|
|
2770
|
+
text: filePlaceholder(block.attachment)
|
|
2771
|
+
});
|
|
2726
2772
|
return parts.length === 0 ? [{
|
|
2727
2773
|
type: "input_text",
|
|
2728
2774
|
text: ""
|
|
@@ -2997,6 +3043,8 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
2997
3043
|
cooldownWrites = Promise.resolve();
|
|
2998
3044
|
/** key fingerprint → cooldown deadline and the rejection that caused it. */
|
|
2999
3045
|
cooldowns = /* @__PURE__ */ new Map();
|
|
3046
|
+
sticky = /* @__PURE__ */ new Map();
|
|
3047
|
+
routing = [];
|
|
3000
3048
|
warnedTools = false;
|
|
3001
3049
|
constructor(deps) {
|
|
3002
3050
|
super();
|
|
@@ -3048,6 +3096,10 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3048
3096
|
};
|
|
3049
3097
|
});
|
|
3050
3098
|
}
|
|
3099
|
+
/** Recent routing decisions, newest first; no keys or prompt content. */
|
|
3100
|
+
routingTrace() {
|
|
3101
|
+
return [...this.routing];
|
|
3102
|
+
}
|
|
3051
3103
|
async resolveModel(provider, model) {
|
|
3052
3104
|
const config = this.deps.config();
|
|
3053
3105
|
const meta = effectiveModel(model, config);
|
|
@@ -3067,14 +3119,31 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3067
3119
|
};
|
|
3068
3120
|
}
|
|
3069
3121
|
stream(options) {
|
|
3070
|
-
return this.run(options);
|
|
3122
|
+
return this.run(options, false);
|
|
3071
3123
|
}
|
|
3072
|
-
async *run(options) {
|
|
3073
|
-
|
|
3124
|
+
async *run(options, fallback) {
|
|
3125
|
+
const record = this.deps.recordRequest ?? ((provider, model, purpose, account, reason) => recordRequest(provider, model, Date.now(), purpose, account, reason));
|
|
3074
3126
|
const config = this.deps.config();
|
|
3075
3127
|
const model = effectiveModel(options.model, config);
|
|
3076
|
-
const keys = orderKeysForModel(this.deps.resolveKeys !== void 0 ? await this.deps.resolveKeys(options.provider) : [await this.deps.resolveKey(options.provider)], options.model, config.modelAccountRules);
|
|
3077
3128
|
const sessionId = openCodeSessionId(options);
|
|
3129
|
+
const rawKeys = orderKeysForModel(this.deps.resolveKeys !== void 0 ? await this.deps.resolveKeys(options.provider) : [await this.deps.resolveKey(options.provider)], options.model, config.modelAccountRules);
|
|
3130
|
+
const rule = matchModelAccount(options.model, config.modelAccountRules);
|
|
3131
|
+
let keys = rawKeys;
|
|
3132
|
+
let reason = rule === void 0 ? "default" : "rule";
|
|
3133
|
+
if (fallback) reason = "quota-fallback";
|
|
3134
|
+
else if (config.activeAccount !== "") reason = "pinned";
|
|
3135
|
+
else if (config.stickySession && rule === void 0) {
|
|
3136
|
+
const previous = this.sticky.get(sessionId);
|
|
3137
|
+
if (previous !== void 0) {
|
|
3138
|
+
const preferred = rawKeys.find((entry) => this.keyId(entry.key) === previous.fingerprint);
|
|
3139
|
+
if (preferred !== void 0) {
|
|
3140
|
+
keys = [preferred, ...rawKeys.filter((entry) => entry !== preferred)];
|
|
3141
|
+
reason = "sticky";
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
const initial = keys.find((entry) => fallback || !this.coolingDown(entry.key)) ?? keys[0];
|
|
3146
|
+
if (initial !== void 0) record(options.provider, options.model, options.purpose, initial.label ?? "default", reason);
|
|
3078
3147
|
const url = endpointFor(options.provider, model.transport, options.model, config);
|
|
3079
3148
|
let images;
|
|
3080
3149
|
if (hasImages(options)) {
|
|
@@ -3096,8 +3165,11 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3096
3165
|
}
|
|
3097
3166
|
const body = model.transport === "openai-chat" ? buildChatBody(bodyOptions, model, config, images) : model.transport === "anthropic" ? buildAnthropicBody(bodyOptions, model, config, images) : model.transport === "openai-responses" ? buildResponsesBody(bodyOptions, model, config, images) : buildGoogleBody(bodyOptions, model, config, images);
|
|
3098
3167
|
let lastError;
|
|
3168
|
+
let lastAccount = initial?.label ?? "default";
|
|
3099
3169
|
for (const [index, entry] of keys.entries()) {
|
|
3100
|
-
if (this.coolingDown(entry.key)) continue;
|
|
3170
|
+
if (!fallback && this.coolingDown(entry.key)) continue;
|
|
3171
|
+
lastAccount = entry.label ?? "default";
|
|
3172
|
+
const attemptReason = index === 0 ? reason : "rotation";
|
|
3101
3173
|
let started = false;
|
|
3102
3174
|
let responseFailure = false;
|
|
3103
3175
|
try {
|
|
@@ -3108,12 +3180,22 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3108
3180
|
lastError = error;
|
|
3109
3181
|
const canRotate = index < keys.length - 1;
|
|
3110
3182
|
if (error.code === "INVALID_CREDENTIAL" || error.code === "RATE_LIMIT" || error.code === "QUOTA") {
|
|
3111
|
-
this.markCooldown(entry.key, response.status, response.headers, error.code);
|
|
3183
|
+
if (!fallback) this.markCooldown(entry.key, response.status, response.headers, error.code);
|
|
3112
3184
|
responseFailure = true;
|
|
3113
3185
|
if (canRotate) continue;
|
|
3114
3186
|
}
|
|
3115
3187
|
throw error;
|
|
3116
3188
|
}
|
|
3189
|
+
this.rememberSticky(sessionId, options, entry);
|
|
3190
|
+
this.pushTrace({
|
|
3191
|
+
at: Date.now(),
|
|
3192
|
+
provider: options.provider,
|
|
3193
|
+
model: options.model,
|
|
3194
|
+
account: entry.label ?? "default",
|
|
3195
|
+
reason: attemptReason,
|
|
3196
|
+
fallback,
|
|
3197
|
+
ok: true
|
|
3198
|
+
});
|
|
3117
3199
|
for await (const chunk of this.parse(response, model.transport, config, options.signal)) {
|
|
3118
3200
|
started = true;
|
|
3119
3201
|
yield chunk;
|
|
@@ -3124,11 +3206,53 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3124
3206
|
const classified = error instanceof LlmError ? error : new LlmError(error instanceof Error ? error.message : "OpenCode request failed", "TRANSPORT", { cause: error });
|
|
3125
3207
|
lastError = classified;
|
|
3126
3208
|
const transient = this.isTransient(classified);
|
|
3127
|
-
if (transient && !responseFailure) this.markCooldown(entry.key, 0);
|
|
3128
|
-
if (!(index < keys.length - 1 && transient))
|
|
3209
|
+
if (transient && !responseFailure && !fallback) this.markCooldown(entry.key, 0);
|
|
3210
|
+
if (!(index < keys.length - 1 && transient)) {
|
|
3211
|
+
if (!fallback && config.quotaFallbackModel !== "" && options.provider === "opencode-go" && classified.code === "QUOTA") break;
|
|
3212
|
+
throw classified;
|
|
3213
|
+
}
|
|
3129
3214
|
}
|
|
3130
3215
|
}
|
|
3131
|
-
|
|
3216
|
+
const finalError = lastError ?? new LlmError("llm-opencode: every configured account is cooling down after provider rejections", "RATE_LIMIT");
|
|
3217
|
+
this.pushTrace({
|
|
3218
|
+
at: Date.now(),
|
|
3219
|
+
provider: options.provider,
|
|
3220
|
+
model: options.model,
|
|
3221
|
+
account: lastAccount,
|
|
3222
|
+
reason,
|
|
3223
|
+
fallback,
|
|
3224
|
+
ok: false,
|
|
3225
|
+
error: finalError.code
|
|
3226
|
+
});
|
|
3227
|
+
if (!fallback && config.quotaFallbackModel !== "" && options.provider === "opencode-go" && (lastError === void 0 || lastError.code === "QUOTA")) try {
|
|
3228
|
+
yield* this.run({
|
|
3229
|
+
...options,
|
|
3230
|
+
provider: "opencode",
|
|
3231
|
+
model: config.quotaFallbackModel
|
|
3232
|
+
}, true);
|
|
3233
|
+
return;
|
|
3234
|
+
} catch {
|
|
3235
|
+
throw finalError;
|
|
3236
|
+
}
|
|
3237
|
+
throw finalError;
|
|
3238
|
+
}
|
|
3239
|
+
rememberSticky(sessionId, options, entry) {
|
|
3240
|
+
if (!this.deps.config().stickySession) return;
|
|
3241
|
+
if (!this.sticky.has(sessionId) && this.sticky.size >= 500) {
|
|
3242
|
+
const oldest = this.sticky.keys().next().value;
|
|
3243
|
+
if (oldest !== void 0) this.sticky.delete(oldest);
|
|
3244
|
+
}
|
|
3245
|
+
this.sticky.set(sessionId, {
|
|
3246
|
+
fingerprint: this.keyId(entry.key),
|
|
3247
|
+
label: entry.label ?? "default",
|
|
3248
|
+
provider: options.provider,
|
|
3249
|
+
model: options.model,
|
|
3250
|
+
at: Date.now()
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
pushTrace(entry) {
|
|
3254
|
+
this.routing.unshift(entry);
|
|
3255
|
+
if (this.routing.length > 20) this.routing.length = 20;
|
|
3132
3256
|
}
|
|
3133
3257
|
parse(response, transport, config, signal) {
|
|
3134
3258
|
if (transport === "anthropic") return parseAnthropicStream(response, config.streamIdleTimeoutMs, signal);
|
|
@@ -4165,6 +4289,8 @@ function publicConfig(config) {
|
|
|
4165
4289
|
pattern: rule.pattern,
|
|
4166
4290
|
account: rule.account
|
|
4167
4291
|
})),
|
|
4292
|
+
quotaFallbackModel: config.quotaFallbackModel,
|
|
4293
|
+
stickySession: config.stickySession,
|
|
4168
4294
|
activeAccount: config.activeAccount,
|
|
4169
4295
|
showSidebarQuota: config.showSidebarQuota,
|
|
4170
4296
|
userAgent: config.userAgent
|
|
@@ -4349,6 +4475,8 @@ function configOps(patch) {
|
|
|
4349
4475
|
if ("maxOutputTokens" in patch) set("maxOutputTokens", patch.maxOutputTokens);
|
|
4350
4476
|
if ("accounts" in patch) set("accounts", cleanAccounts(patch.accounts));
|
|
4351
4477
|
if ("modelAccountRules" in patch) set("modelAccountRules", cleanModelAccountRules(patch.modelAccountRules));
|
|
4478
|
+
if ("quotaFallbackModel" in patch) set("quotaFallbackModel", patch.quotaFallbackModel);
|
|
4479
|
+
if ("stickySession" in patch) set("stickySession", patch.stickySession === true);
|
|
4352
4480
|
if ("activeAccount" in patch) set("activeAccount", patch.activeAccount);
|
|
4353
4481
|
if ("showSidebarQuota" in patch) set("showSidebarQuota", patch.showSidebarQuota === true);
|
|
4354
4482
|
if ("userAgent" in patch) set("userAgent", patch.userAgent);
|
|
@@ -4451,6 +4579,7 @@ async function buildState(ctx, deps) {
|
|
|
4451
4579
|
accounts,
|
|
4452
4580
|
models,
|
|
4453
4581
|
usage: usage ?? null,
|
|
4582
|
+
routing: deps.adapter.routingTrace(),
|
|
4454
4583
|
auth: {
|
|
4455
4584
|
entry: config.openCodeAuthEntry,
|
|
4456
4585
|
hasStoredKey: await readOpenCodeAuthKey(config.openCodeAuthEntry) !== void 0,
|
|
@@ -4608,15 +4737,17 @@ function applyRoutes(ctx, web, deps) {
|
|
|
4608
4737
|
try {
|
|
4609
4738
|
const body = await readJson(req);
|
|
4610
4739
|
const entry = typeof body.entry === "string" && body.entry.trim() !== "" ? body.entry.trim() : deps.config().openCodeAuthEntry;
|
|
4740
|
+
const ref = typeof body.ref === "string" && body.ref.trim() !== "" ? body.ref.trim() : deps.config().apiKeyEnv;
|
|
4611
4741
|
const key = await readOpenCodeAuthKey(entry);
|
|
4612
4742
|
if (key === void 0) {
|
|
4613
4743
|
json(res, 404, { error: `no key in auth.json entry "${entry}"` });
|
|
4614
4744
|
return;
|
|
4615
4745
|
}
|
|
4616
|
-
await credentials.set(credentialRef(
|
|
4746
|
+
await credentials.set(credentialRef(ref), key);
|
|
4617
4747
|
json(res, 200, {
|
|
4618
4748
|
ok: true,
|
|
4619
|
-
entry
|
|
4749
|
+
entry,
|
|
4750
|
+
ref
|
|
4620
4751
|
});
|
|
4621
4752
|
} catch (error) {
|
|
4622
4753
|
json(res, 500, { error: error instanceof Error ? error.message : "import failed" });
|
|
@@ -4805,6 +4936,27 @@ function buildOpenCodeTuiSection(deps) {
|
|
|
4805
4936
|
value: text.trim() === "true"
|
|
4806
4937
|
})
|
|
4807
4938
|
},
|
|
4939
|
+
{
|
|
4940
|
+
path: ["quotaFallbackModel"],
|
|
4941
|
+
group: "models",
|
|
4942
|
+
kind: "text",
|
|
4943
|
+
label: "Quota fallback model",
|
|
4944
|
+
hint: "Free model id used once after a Go quota error; empty disables it.",
|
|
4945
|
+
format: () => deps.quotaFallbackModel(),
|
|
4946
|
+
parse: setOrClear
|
|
4947
|
+
},
|
|
4948
|
+
{
|
|
4949
|
+
path: ["stickySession"],
|
|
4950
|
+
group: "advanced",
|
|
4951
|
+
kind: "boolean",
|
|
4952
|
+
label: "Sticky account per conversation",
|
|
4953
|
+
hint: "Prefer the last successful account for the same conversation.",
|
|
4954
|
+
format: (value) => String(value === void 0 ? deps.stickySession() : value !== false),
|
|
4955
|
+
parse: (text) => ({
|
|
4956
|
+
kind: "set",
|
|
4957
|
+
value: text.trim() === "true"
|
|
4958
|
+
})
|
|
4959
|
+
},
|
|
4808
4960
|
{
|
|
4809
4961
|
path: ["activeAccount"],
|
|
4810
4962
|
group: "advanced",
|
|
@@ -4874,7 +5026,9 @@ function applyOpenCodeTuiSettings(ctx, deps) {
|
|
|
4874
5026
|
accounts: deps.accountSlots().map((slot) => [slot.id, slot.label]),
|
|
4875
5027
|
visibleModels: deps.visibleModels(),
|
|
4876
5028
|
activeAccount: deps.activeAccount(),
|
|
4877
|
-
filterModelsByPlan: deps.filterModelsByPlan()
|
|
5029
|
+
filterModelsByPlan: deps.filterModelsByPlan(),
|
|
5030
|
+
quotaFallbackModel: deps.quotaFallbackModel(),
|
|
5031
|
+
stickySession: deps.stickySession()
|
|
4878
5032
|
});
|
|
4879
5033
|
let lastSignature = signature();
|
|
4880
5034
|
const refresh = () => {
|
|
@@ -5053,7 +5207,9 @@ function apply(ctx, config) {
|
|
|
5053
5207
|
}))],
|
|
5054
5208
|
visibleModels: () => current().visibleModels,
|
|
5055
5209
|
activeAccount: () => current().activeAccount,
|
|
5056
|
-
filterModelsByPlan: () => current().filterModelsByPlan
|
|
5210
|
+
filterModelsByPlan: () => current().filterModelsByPlan,
|
|
5211
|
+
quotaFallbackModel: () => current().quotaFallbackModel,
|
|
5212
|
+
stickySession: () => current().stickySession
|
|
5057
5213
|
});
|
|
5058
5214
|
});
|
|
5059
5215
|
ctx.inject(["webServer"], (webCtx) => {
|