dsh-provider-qoder 0.1.0 → 0.2.1
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/README.md +10 -5
- package/README_CN.md +10 -5
- package/lib/client.js +189 -84
- package/lib/index.d.ts +61 -341
- package/lib/index.js +1691 -837
- package/package.json +3 -1
package/lib/index.js
CHANGED
|
@@ -1,23 +1,203 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
|
-
import { EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ReasoningEffortId, ToolCallId, attributionHeaders } from "@deepseek-ai/dsh-llm";
|
|
3
|
-
import crypto from "node:crypto";
|
|
2
|
+
import { EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ProviderRequestId, ReasoningEffortId, ToolCallId, attributionHeaders, contentHasImage } from "@deepseek-ai/dsh-llm";
|
|
3
|
+
import crypto, { createHash } from "node:crypto";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
8
|
-
//#region src/
|
|
8
|
+
//#region src/qoder/catalog.ts
|
|
9
|
+
const discoveredMetadataKeys = [
|
|
10
|
+
"description",
|
|
11
|
+
"source",
|
|
12
|
+
"isReasoning",
|
|
13
|
+
"supportsEffort",
|
|
14
|
+
"reasoningEfforts",
|
|
15
|
+
"defaultReasoningEffort",
|
|
16
|
+
"priceFactor",
|
|
17
|
+
"contextOptions",
|
|
18
|
+
"maxContextWindow",
|
|
19
|
+
"supportsImages"
|
|
20
|
+
];
|
|
21
|
+
const reasoningEffortOrder = /* @__PURE__ */ new Map([
|
|
22
|
+
["low", 0],
|
|
23
|
+
["medium", 1],
|
|
24
|
+
["high", 2],
|
|
25
|
+
["xhigh", 3],
|
|
26
|
+
["max", 4]
|
|
27
|
+
]);
|
|
28
|
+
function positiveNumber(value) {
|
|
29
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
30
|
+
}
|
|
31
|
+
function contextOptionsOf(value) {
|
|
32
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
33
|
+
const options = {};
|
|
34
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
35
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
|
|
36
|
+
const entry = raw;
|
|
37
|
+
const tokenCount = positiveNumber(entry.token_count);
|
|
38
|
+
const isDefault = typeof entry.is_default === "boolean" ? entry.is_default : void 0;
|
|
39
|
+
if (tokenCount === void 0) continue;
|
|
40
|
+
options[key] = {
|
|
41
|
+
...tokenCount === void 0 ? {} : { tokenCount },
|
|
42
|
+
...isDefault === void 0 ? {} : { isDefault }
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (Object.keys(options).length === 0) return void 0;
|
|
46
|
+
return options;
|
|
47
|
+
}
|
|
48
|
+
function reasoningEffortsOf(value) {
|
|
49
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
|
|
50
|
+
const enabled = value.enabled;
|
|
51
|
+
if (typeof enabled !== "object" || enabled === null || Array.isArray(enabled)) return {};
|
|
52
|
+
const rawEfforts = enabled.efforts;
|
|
53
|
+
if (typeof rawEfforts !== "object" || rawEfforts === null || Array.isArray(rawEfforts)) return {};
|
|
54
|
+
const efforts = [];
|
|
55
|
+
let defaultEffort;
|
|
56
|
+
for (const [id, raw] of Object.entries(rawEfforts)) {
|
|
57
|
+
if (!id.trim() || typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
|
|
58
|
+
const entry = raw;
|
|
59
|
+
efforts.push({
|
|
60
|
+
id,
|
|
61
|
+
name: id,
|
|
62
|
+
...typeof entry.description === "string" && entry.description.trim() ? { description: entry.description.trim() } : {}
|
|
63
|
+
});
|
|
64
|
+
if (entry.is_default === true) defaultEffort = id;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
...efforts.length === 0 ? {} : { efforts: efforts.sort((left, right) => (reasoningEffortOrder.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (reasoningEffortOrder.get(right.id) ?? Number.MAX_SAFE_INTEGER)) },
|
|
68
|
+
...defaultEffort === void 0 ? {} : { defaultEffort }
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function thinkingDefault(value, fallback, onConflict) {
|
|
72
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return fallback;
|
|
73
|
+
const config = value;
|
|
74
|
+
const isDefault = (entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry) && entry.is_default === true;
|
|
75
|
+
const enabled = isDefault(config.enabled);
|
|
76
|
+
const disabled = isDefault(config.disabled);
|
|
77
|
+
if (enabled && disabled) onConflict?.("thinking-defaults");
|
|
78
|
+
return enabled === disabled ? fallback : enabled;
|
|
79
|
+
}
|
|
80
|
+
function normalizeQoderModels(payload, onConflict) {
|
|
81
|
+
if (typeof payload !== "object" || payload === null || !Array.isArray(payload.assistant)) return [];
|
|
82
|
+
const models = [];
|
|
83
|
+
const seen = /* @__PURE__ */ new Set();
|
|
84
|
+
for (const raw of payload.assistant) {
|
|
85
|
+
if (typeof raw !== "object" || raw === null || raw.enable !== true) continue;
|
|
86
|
+
const id = typeof raw.key === "string" ? raw.key.trim() : "";
|
|
87
|
+
if (!id || seen.has(id)) continue;
|
|
88
|
+
seen.add(id);
|
|
89
|
+
const contextOptions = contextOptionsOf(raw.context_config);
|
|
90
|
+
const defaultOptions = Object.values(contextOptions ?? {}).filter((option) => option.isDefault && option.tokenCount !== void 0);
|
|
91
|
+
if (defaultOptions.length > 1) onConflict?.("context-defaults");
|
|
92
|
+
const contextWindow = (defaultOptions.length === 1 ? defaultOptions[0].tokenCount : void 0) ?? positiveNumber(raw.max_input_tokens) ?? 18e4;
|
|
93
|
+
const maxContextWindow = Math.max(positiveNumber(raw.max_input_tokens) ?? 0, contextWindow, ...Object.values(contextOptions ?? {}).map((option) => option.tokenCount ?? 0));
|
|
94
|
+
const isReasoning = thinkingDefault(raw.thinking_config, raw.is_reasoning === true, onConflict);
|
|
95
|
+
const priceFactor = typeof raw.price_factor === "number" && Number.isFinite(raw.price_factor) && raw.price_factor >= 0 ? raw.price_factor : void 0;
|
|
96
|
+
const reasoning = reasoningEffortsOf(raw.thinking_config);
|
|
97
|
+
models.push({
|
|
98
|
+
id,
|
|
99
|
+
name: typeof raw.display_name === "string" && raw.display_name.trim() ? raw.display_name.trim() : id,
|
|
100
|
+
contextWindow,
|
|
101
|
+
maxContextWindow,
|
|
102
|
+
maxTokens: positiveNumber(raw.max_output_tokens) ?? 32768,
|
|
103
|
+
source: typeof raw.source === "string" && raw.source.trim() ? raw.source.trim() : "system",
|
|
104
|
+
isReasoning,
|
|
105
|
+
supportsEffort: reasoning.efforts !== void 0,
|
|
106
|
+
supportsImages: raw.is_vl === true,
|
|
107
|
+
...reasoning.efforts === void 0 ? {} : { reasoningEfforts: reasoning.efforts },
|
|
108
|
+
...reasoning.defaultEffort === void 0 ? {} : { defaultReasoningEffort: reasoning.defaultEffort },
|
|
109
|
+
...priceFactor === void 0 ? {} : { priceFactor },
|
|
110
|
+
...contextOptions === void 0 ? {} : { contextOptions }
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return models;
|
|
114
|
+
}
|
|
115
|
+
function mergeQoderDiscoveryMetadata(configured, discovered) {
|
|
116
|
+
const catalog = new Map(discovered.map((model) => [model.id, model]));
|
|
117
|
+
return configured.map((model) => {
|
|
118
|
+
const advertised = catalog.get(model.id);
|
|
119
|
+
if (advertised === void 0) return { ...model };
|
|
120
|
+
const merged = { ...model };
|
|
121
|
+
if (advertised.contextWindow !== void 0) merged.contextWindow = Math.min(model.contextWindow ?? advertised.contextWindow, advertised.contextWindow);
|
|
122
|
+
for (const key of discoveredMetadataKeys) delete merged[key];
|
|
123
|
+
for (const key of discoveredMetadataKeys) if (advertised[key] !== void 0) Object.assign(merged, { [key]: advertised[key] });
|
|
124
|
+
return merged;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function hasSameQoderDiscoveryMetadata(left, right) {
|
|
128
|
+
return left?.length === right.length && left.every((model, index) => {
|
|
129
|
+
const candidate = right[index];
|
|
130
|
+
return candidate?.id === model.id && candidate.contextWindow === model.contextWindow && discoveredMetadataKeys.every((key) => JSON.stringify(model[key]) === JSON.stringify(candidate[key]));
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
const defaultMaxTokens = 32768;
|
|
134
|
+
const defaultModels = [
|
|
135
|
+
{
|
|
136
|
+
id: "cmodel",
|
|
137
|
+
name: "Cantus (Qoder)",
|
|
138
|
+
description: "Default Global Qoder subscription model for quick validation",
|
|
139
|
+
contextWindow: 1e6,
|
|
140
|
+
maxTokens: defaultMaxTokens,
|
|
141
|
+
supportsImages: true
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: "auto",
|
|
145
|
+
name: "Qoder Auto",
|
|
146
|
+
description: "Server-routed Global Qoder model pool",
|
|
147
|
+
contextWindow: 18e4,
|
|
148
|
+
maxTokens: defaultMaxTokens,
|
|
149
|
+
supportsImages: true
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: "ultimate",
|
|
153
|
+
name: "Qoder Ultimate",
|
|
154
|
+
description: "Highest-capability Global Qoder model pool",
|
|
155
|
+
contextWindow: 1e6,
|
|
156
|
+
maxTokens: defaultMaxTokens,
|
|
157
|
+
supportsImages: true
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: "performance",
|
|
161
|
+
name: "Qoder Performance",
|
|
162
|
+
description: "Performance-oriented Global Qoder model pool",
|
|
163
|
+
contextWindow: 1e6,
|
|
164
|
+
maxTokens: defaultMaxTokens,
|
|
165
|
+
supportsImages: true
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: "efficient",
|
|
169
|
+
name: "Qoder Efficient",
|
|
170
|
+
description: "Efficiency-oriented Global Qoder model pool",
|
|
171
|
+
contextWindow: 18e4,
|
|
172
|
+
maxTokens: defaultMaxTokens,
|
|
173
|
+
supportsImages: true
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: "lite",
|
|
177
|
+
name: "Qoder Lite",
|
|
178
|
+
description: "Basic Global Qoder model pool",
|
|
179
|
+
contextWindow: 18e4,
|
|
180
|
+
maxTokens: defaultMaxTokens
|
|
181
|
+
}
|
|
182
|
+
];
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/qoder/transport/endpoints.ts
|
|
9
185
|
const qoderRegionEndpoints = {
|
|
10
186
|
global: {
|
|
11
187
|
baseUrl: "https://api3.qoder.sh/",
|
|
12
|
-
openApiUrl: "https://openapi.qoder.sh"
|
|
188
|
+
openApiUrl: "https://openapi.qoder.sh",
|
|
189
|
+
centerUrl: "https://center.qoder.sh"
|
|
13
190
|
},
|
|
14
191
|
china: {
|
|
15
192
|
baseUrl: "https://gateway.qoder.com.cn/",
|
|
16
|
-
openApiUrl: "https://openapi.qoder.com.cn"
|
|
193
|
+
openApiUrl: "https://openapi.qoder.com.cn",
|
|
194
|
+
centerUrl: "https://gateway.qoder.com.cn"
|
|
17
195
|
}
|
|
18
196
|
};
|
|
19
|
-
|
|
20
|
-
const
|
|
197
|
+
/** Signed path of the center image upload route; it never carries an `/algo` prefix. */
|
|
198
|
+
const qoderImageUploadPath = "/api/v2/image/upload";
|
|
199
|
+
qoderRegionEndpoints.global.baseUrl;
|
|
200
|
+
qoderRegionEndpoints.global.openApiUrl;
|
|
21
201
|
function resolveQoderEndpoints(region = "global") {
|
|
22
202
|
return qoderRegionEndpoints[region] ?? qoderRegionEndpoints.global;
|
|
23
203
|
}
|
|
@@ -29,6 +209,11 @@ function getQoderModelListUrl(region = "global") {
|
|
|
29
209
|
const { baseUrl } = resolveQoderEndpoints(region);
|
|
30
210
|
return `${baseUrl}algo/api/v2/model/list?Encode=1`;
|
|
31
211
|
}
|
|
212
|
+
function getQoderImageUploadUrl(region = "global", requestId) {
|
|
213
|
+
const { centerUrl } = resolveQoderEndpoints(region);
|
|
214
|
+
const base = `${centerUrl.replace(/\/+$/u, "")}/algo${qoderImageUploadPath}`;
|
|
215
|
+
return requestId === void 0 ? base : `${base}?request_id=${encodeURIComponent(requestId)}`;
|
|
216
|
+
}
|
|
32
217
|
function getQoderExchangeUrl(region = "global") {
|
|
33
218
|
const { openApiUrl } = resolveQoderEndpoints(region);
|
|
34
219
|
return `${openApiUrl}/api/v1/jobToken/exchange`;
|
|
@@ -41,12 +226,20 @@ function getQoderUsageUrl(region = "global") {
|
|
|
41
226
|
const { openApiUrl } = resolveQoderEndpoints(region);
|
|
42
227
|
return `${openApiUrl}/api/v2/quota/usage`;
|
|
43
228
|
}
|
|
229
|
+
function getQoderUserPlanUrl(region = "global") {
|
|
230
|
+
const { openApiUrl } = resolveQoderEndpoints(region);
|
|
231
|
+
return `${openApiUrl}/api/v2/user/plan`;
|
|
232
|
+
}
|
|
233
|
+
function getQoderUserStatusUrl(region = "global") {
|
|
234
|
+
const { openApiUrl } = resolveQoderEndpoints(region);
|
|
235
|
+
return `${openApiUrl}/api/v3/user/status`;
|
|
236
|
+
}
|
|
44
237
|
//#endregion
|
|
45
|
-
//#region src/errors.ts
|
|
238
|
+
//#region src/qoder/errors.ts
|
|
46
239
|
/**
|
|
47
240
|
* DSH-compatible LLM error representation.
|
|
48
241
|
*
|
|
49
|
-
* @module dsh-provider-qoder/errors
|
|
242
|
+
* @module dsh-provider-qoder/qoder/errors
|
|
50
243
|
*/
|
|
51
244
|
var QoderLlmError = class extends LlmError {
|
|
52
245
|
constructor(message, code = "UNKNOWN_ERROR", options) {
|
|
@@ -57,11 +250,17 @@ function qoderHttpError(message, response) {
|
|
|
57
250
|
const { status } = response;
|
|
58
251
|
const code = status === 401 || status === 403 ? "AUTH" : status === 408 ? "TIMEOUT" : status === 429 ? "RATE_LIMIT" : status >= 500 && status <= 599 ? "SERVER" : status >= 400 && status <= 499 ? "INVALID_REQUEST" : "PROVIDER_ERROR";
|
|
59
252
|
const providerRetryAfterMs = retryAfterMs(response.headers?.get("retry-after") ?? null);
|
|
253
|
+
const requestId = qoderRequestId(response.headers);
|
|
60
254
|
return new QoderLlmError(message, code, {
|
|
61
255
|
status,
|
|
62
|
-
...providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs }
|
|
256
|
+
...providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs },
|
|
257
|
+
...requestId === void 0 ? {} : { requestId }
|
|
63
258
|
});
|
|
64
259
|
}
|
|
260
|
+
function qoderRequestId(headers) {
|
|
261
|
+
const normalized = (headers?.get("x-request-id") ?? headers?.get("request-id") ?? headers?.get("x-amzn-requestid"))?.trim();
|
|
262
|
+
return normalized ? ProviderRequestId(normalized) : void 0;
|
|
263
|
+
}
|
|
65
264
|
function retryAfterMs(value, nowMs = Date.now()) {
|
|
66
265
|
const normalized = value?.trim();
|
|
67
266
|
if (!normalized) return void 0;
|
|
@@ -75,7 +274,37 @@ function retryAfterMs(value, nowMs = Date.now()) {
|
|
|
75
274
|
return delayMs > 0 ? delayMs : void 0;
|
|
76
275
|
}
|
|
77
276
|
//#endregion
|
|
78
|
-
//#region src/
|
|
277
|
+
//#region src/qoder/transport/machine-id.ts
|
|
278
|
+
function defaultMachineIdPaths() {
|
|
279
|
+
return [join(homedir(), ".qoder", ".auth", "machine_id"), join(homedir(), ".dsh", "qoder", "machine_id")];
|
|
280
|
+
}
|
|
281
|
+
/** Read Qoder's machine id or create the DSH-owned fallback. */
|
|
282
|
+
function getMachineId(paths = defaultMachineIdPaths()) {
|
|
283
|
+
for (const path of paths) {
|
|
284
|
+
if (!existsSync(path)) continue;
|
|
285
|
+
try {
|
|
286
|
+
const value = readFileSync(path, "utf8").trim();
|
|
287
|
+
if (value) return value;
|
|
288
|
+
} catch {}
|
|
289
|
+
}
|
|
290
|
+
const machineId = crypto.randomUUID();
|
|
291
|
+
const savePath = paths.at(-1);
|
|
292
|
+
if (savePath !== void 0) try {
|
|
293
|
+
mkdirSync(dirname(savePath), { recursive: true });
|
|
294
|
+
writeFileSync(savePath, machineId, {
|
|
295
|
+
encoding: "utf8",
|
|
296
|
+
flag: "wx"
|
|
297
|
+
});
|
|
298
|
+
} catch {
|
|
299
|
+
try {
|
|
300
|
+
const existing = readFileSync(savePath, "utf8").trim();
|
|
301
|
+
if (existing) return existing;
|
|
302
|
+
} catch {}
|
|
303
|
+
}
|
|
304
|
+
return machineId;
|
|
305
|
+
}
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region src/qoder/transport/logging.ts
|
|
79
308
|
const sensitiveKey = /(?:authorization|credential|password|secret|token)/iu;
|
|
80
309
|
const identifierKey = /^(?:accountId|id|uid|userId)$/iu;
|
|
81
310
|
const tokenValue = /\b(?:jrt|jt|pt)-[\w.-]+\b/giu;
|
|
@@ -122,224 +351,22 @@ function redactLogPayload(text) {
|
|
|
122
351
|
return redactLogValue(text);
|
|
123
352
|
}
|
|
124
353
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
function getMachineId(paths = defaultMachineIdPaths()) {
|
|
132
|
-
for (const path of paths) {
|
|
133
|
-
if (!existsSync(path)) continue;
|
|
134
|
-
try {
|
|
135
|
-
const value = readFileSync(path, "utf8").trim();
|
|
136
|
-
if (value) return value;
|
|
137
|
-
} catch {}
|
|
138
|
-
}
|
|
139
|
-
const machineId = crypto.randomUUID();
|
|
140
|
-
const savePath = paths.at(-1);
|
|
141
|
-
if (savePath !== void 0) try {
|
|
142
|
-
mkdirSync(dirname(savePath), { recursive: true });
|
|
143
|
-
writeFileSync(savePath, machineId, {
|
|
144
|
-
encoding: "utf8",
|
|
145
|
-
flag: "wx"
|
|
146
|
-
});
|
|
147
|
-
} catch {
|
|
148
|
-
try {
|
|
149
|
-
const existing = readFileSync(savePath, "utf8").trim();
|
|
150
|
-
if (existing) return existing;
|
|
151
|
-
} catch {}
|
|
152
|
-
}
|
|
153
|
-
return machineId;
|
|
354
|
+
/** Log one successfully parsed non-stream response through the shared redaction policy. */
|
|
355
|
+
function logParsedResponse(logger, operation, result) {
|
|
356
|
+
logger?.debug?.("[Qoder Response] Parsed", redactLogValue({
|
|
357
|
+
operation,
|
|
358
|
+
result
|
|
359
|
+
}));
|
|
154
360
|
}
|
|
155
361
|
//#endregion
|
|
156
|
-
//#region src/
|
|
157
|
-
const userAgent$1 = "dsh-provider-qoder";
|
|
158
|
-
const expiryBufferMs = 300 * 1e3;
|
|
159
|
-
const defaultExpiryMs = 1440 * 60 * 1e3;
|
|
160
|
-
const defaultAuthTimeoutMs = 15e3;
|
|
161
|
-
function abortedError() {
|
|
162
|
-
return new QoderLlmError("Qoder authentication was aborted.", "ABORTED");
|
|
163
|
-
}
|
|
164
|
-
async function waitForFlight(promise, signal) {
|
|
165
|
-
if (signal === void 0) return promise;
|
|
166
|
-
if (signal.aborted) throw abortedError();
|
|
167
|
-
return new Promise((resolve, reject) => {
|
|
168
|
-
const onAbort = () => reject(abortedError());
|
|
169
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
170
|
-
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
var QoderAuthService = class {
|
|
174
|
-
cache = /* @__PURE__ */ new Map();
|
|
175
|
-
inFlight = /* @__PURE__ */ new Map();
|
|
176
|
-
fetchImpl;
|
|
177
|
-
timeoutMs;
|
|
178
|
-
resolveMachineId;
|
|
179
|
-
resolveRegion;
|
|
180
|
-
region;
|
|
181
|
-
logger;
|
|
182
|
-
constructor(options = {}) {
|
|
183
|
-
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
184
|
-
this.timeoutMs = options.timeoutMs ?? defaultAuthTimeoutMs;
|
|
185
|
-
this.resolveMachineId = options.resolveMachineId ?? getMachineId;
|
|
186
|
-
this.resolveRegion = options.resolveRegion;
|
|
187
|
-
this.region = options.region;
|
|
188
|
-
this.logger = options.logger;
|
|
189
|
-
}
|
|
190
|
-
setRegion(region) {
|
|
191
|
-
this.region = region;
|
|
192
|
-
}
|
|
193
|
-
currentRegion() {
|
|
194
|
-
if (this.resolveRegion) return this.resolveRegion();
|
|
195
|
-
return this.region ?? "global";
|
|
196
|
-
}
|
|
197
|
-
clear(pat) {
|
|
198
|
-
if (pat) {
|
|
199
|
-
for (const key of this.cache.keys()) if (key.endsWith(`:${pat}`) || key === pat) this.cache.delete(key);
|
|
200
|
-
} else this.cache.clear();
|
|
201
|
-
}
|
|
202
|
-
async getCredentials(pat, signal, region) {
|
|
203
|
-
if (!pat || typeof pat !== "string") throw new QoderLlmError("Qoder Personal Access Token is missing or invalid. Configure Qoder in the Qoder settings page.", "MISSING_CREDENTIAL");
|
|
204
|
-
if (signal?.aborted) throw abortedError();
|
|
205
|
-
const effectiveRegion = region ?? this.currentRegion();
|
|
206
|
-
const cacheKey = `${effectiveRegion}:${pat}`;
|
|
207
|
-
const cached = this.cache.get(cacheKey);
|
|
208
|
-
if (cached && cached.expiresAt > Date.now() + expiryBufferMs) return cached.creds;
|
|
209
|
-
let entry = this.inFlight.get(cacheKey);
|
|
210
|
-
if (entry === void 0) {
|
|
211
|
-
const controller = new AbortController();
|
|
212
|
-
const created = {};
|
|
213
|
-
created.controller = controller;
|
|
214
|
-
created.waiters = 0;
|
|
215
|
-
created.settled = false;
|
|
216
|
-
created.timeout = setTimeout(() => controller.abort("authentication timeout"), this.timeoutMs);
|
|
217
|
-
created.promise = this.exchangeAndResolve(pat, controller.signal, effectiveRegion).finally(() => {
|
|
218
|
-
created.settled = true;
|
|
219
|
-
clearTimeout(created.timeout);
|
|
220
|
-
if (this.inFlight.get(cacheKey) === created) this.inFlight.delete(cacheKey);
|
|
221
|
-
});
|
|
222
|
-
entry = created;
|
|
223
|
-
this.inFlight.set(cacheKey, entry);
|
|
224
|
-
}
|
|
225
|
-
entry.waiters++;
|
|
226
|
-
try {
|
|
227
|
-
return await waitForFlight(entry.promise, signal);
|
|
228
|
-
} finally {
|
|
229
|
-
entry.waiters--;
|
|
230
|
-
if (entry.waiters === 0 && !entry.settled) entry.controller.abort("all callers aborted");
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
async exchangeAndResolve(pat, signal, region) {
|
|
234
|
-
let jobToken;
|
|
235
|
-
let expiresAt = Date.now() + defaultExpiryMs;
|
|
236
|
-
try {
|
|
237
|
-
const url = getQoderExchangeUrl(region);
|
|
238
|
-
this.logger?.debug?.("[Qoder Auth] Exchanging PAT for job token", {
|
|
239
|
-
url,
|
|
240
|
-
region
|
|
241
|
-
});
|
|
242
|
-
const response = await this.fetchImpl(url, {
|
|
243
|
-
method: "POST",
|
|
244
|
-
headers: {
|
|
245
|
-
"content-type": "application/json",
|
|
246
|
-
"accept": "application/json",
|
|
247
|
-
"user-agent": userAgent$1,
|
|
248
|
-
"cosy-version": "1.0.1",
|
|
249
|
-
"cosy-clienttype": "5"
|
|
250
|
-
},
|
|
251
|
-
body: JSON.stringify({ personal_token: pat }),
|
|
252
|
-
signal
|
|
253
|
-
});
|
|
254
|
-
this.logger?.debug?.("[Qoder Auth] Exchange completed", {
|
|
255
|
-
status: response.status,
|
|
256
|
-
statusText: response.statusText
|
|
257
|
-
});
|
|
258
|
-
if (!response.ok) {
|
|
259
|
-
const errorText = await response.text();
|
|
260
|
-
this.logger?.error?.("[Qoder Auth] Exchange failed", redactLogPayload(errorText));
|
|
261
|
-
throw qoderHttpError(`Qoder PAT exchange failed with HTTP status ${response.status}.`, response);
|
|
262
|
-
}
|
|
263
|
-
const data = await response.json();
|
|
264
|
-
if (!data.token) throw new QoderLlmError("Qoder PAT exchange returned no job token.", "AUTH");
|
|
265
|
-
jobToken = data.token;
|
|
266
|
-
if (data.expires_at) {
|
|
267
|
-
const parsed = Date.parse(data.expires_at);
|
|
268
|
-
if (!Number.isNaN(parsed)) expiresAt = parsed;
|
|
269
|
-
} else if (typeof data.expires_in === "number" && data.expires_in > 0) expiresAt = Date.now() + data.expires_in;
|
|
270
|
-
} catch (error) {
|
|
271
|
-
if (error instanceof QoderLlmError) throw error;
|
|
272
|
-
if (signal.aborted) throw new QoderLlmError("Qoder authentication request timed out or was cancelled.", "TIMEOUT");
|
|
273
|
-
throw new QoderLlmError("Qoder PAT exchange network request failed.", "TRANSPORT", { cause: error });
|
|
274
|
-
}
|
|
275
|
-
const userInfo = await this.fetchUserInfo(jobToken, signal, region);
|
|
276
|
-
const creds = {
|
|
277
|
-
userID: userInfo.userID,
|
|
278
|
-
authToken: jobToken,
|
|
279
|
-
name: userInfo.name || "Qoder User",
|
|
280
|
-
email: userInfo.email,
|
|
281
|
-
machineID: this.resolveMachineId()
|
|
282
|
-
};
|
|
283
|
-
const cacheKey = `${region}:${pat}`;
|
|
284
|
-
this.cache.set(cacheKey, {
|
|
285
|
-
creds,
|
|
286
|
-
expiresAt
|
|
287
|
-
});
|
|
288
|
-
return creds;
|
|
289
|
-
}
|
|
290
|
-
async fetchUserInfo(jobToken, signal, region) {
|
|
291
|
-
const url = getQoderUserInfoUrl(region);
|
|
292
|
-
this.logger?.debug?.("[Qoder UserInfo] Requesting subscriber profile", { url });
|
|
293
|
-
try {
|
|
294
|
-
const response = await this.fetchImpl(url, {
|
|
295
|
-
headers: {
|
|
296
|
-
"authorization": `Bearer ${jobToken}`,
|
|
297
|
-
"accept": "application/json",
|
|
298
|
-
"user-agent": userAgent$1,
|
|
299
|
-
"cosy-version": "1.0.1",
|
|
300
|
-
"cosy-clienttype": "5"
|
|
301
|
-
},
|
|
302
|
-
signal
|
|
303
|
-
});
|
|
304
|
-
this.logger?.debug?.("[Qoder UserInfo] Request completed", {
|
|
305
|
-
status: response.status,
|
|
306
|
-
statusText: response.statusText
|
|
307
|
-
});
|
|
308
|
-
const text = await response.text();
|
|
309
|
-
if (!response.ok) {
|
|
310
|
-
this.logger?.error?.("[Qoder UserInfo] Request failed", redactLogPayload(text));
|
|
311
|
-
throw qoderHttpError(`Qoder identity lookup failed with HTTP status ${response.status}.`, response);
|
|
312
|
-
}
|
|
313
|
-
let info;
|
|
314
|
-
try {
|
|
315
|
-
info = JSON.parse(text);
|
|
316
|
-
} catch {
|
|
317
|
-
this.logger?.error?.("[Qoder UserInfo] Invalid JSON response", redactLogPayload(text));
|
|
318
|
-
throw new QoderLlmError("Qoder identity lookup returned invalid JSON.", "AUTH");
|
|
319
|
-
}
|
|
320
|
-
this.logger?.debug?.("[Qoder UserInfo] Subscriber profile resolved", redactLogValue(info));
|
|
321
|
-
if (!info.id) throw new QoderLlmError("Qoder identity lookup returned no user id.", "AUTH");
|
|
322
|
-
return {
|
|
323
|
-
userID: info.id,
|
|
324
|
-
email: info.email ?? "",
|
|
325
|
-
name: info.name ?? info.username ?? ""
|
|
326
|
-
};
|
|
327
|
-
} catch (error) {
|
|
328
|
-
if (error instanceof QoderLlmError) throw error;
|
|
329
|
-
if (signal.aborted) throw new QoderLlmError("Qoder identity lookup timed out or was cancelled.", "TIMEOUT");
|
|
330
|
-
throw new QoderLlmError("Qoder identity lookup network request failed.", "TRANSPORT", { cause: error });
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
//#endregion
|
|
335
|
-
//#region src/cosy.ts
|
|
362
|
+
//#region src/qoder/transport/wire/cosy.ts
|
|
336
363
|
/**
|
|
337
364
|
* COSY authentication headers and signature algorithm for Qoder services.
|
|
338
365
|
*
|
|
339
366
|
* Implements RSA + AES + MD5 signature generation required by the upstream
|
|
340
367
|
* Qoder gateway.
|
|
341
368
|
*
|
|
342
|
-
* @module dsh-provider-qoder/cosy
|
|
369
|
+
* @module dsh-provider-qoder/qoder/transport/wire/cosy
|
|
343
370
|
*/
|
|
344
371
|
const qoderRSAPublicKey = `-----BEGIN PUBLIC KEY-----
|
|
345
372
|
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA8iMH5c02LilrsERw9t6Pv5Nc
|
|
@@ -347,8 +374,8 @@ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA8iMH5c02LilrsERw9t6Pv5Nc
|
|
|
347
374
|
6HRkPJ7S236FZz73In/KVuLnwI8JJ2CbuJap8kvheCCZpmAWpb/cPx/3Vr/J6I17
|
|
348
375
|
XcW+ML9FoCI6AOvOzwIDAQAB
|
|
349
376
|
-----END PUBLIC KEY-----`;
|
|
350
|
-
const qoderIdeVersion = "1.1.
|
|
351
|
-
const
|
|
377
|
+
const qoderIdeVersion = "1.1.47";
|
|
378
|
+
const defaultUserAgent = `qoder/${qoderIdeVersion}`;
|
|
352
379
|
const qoderDataPolicy = "disagree";
|
|
353
380
|
const qoderLoginVersion = "v2";
|
|
354
381
|
const qoderMachineOs = process.platform === "win32" ? process.arch === "arm64" ? "aarch64_windows" : "x86_64_windows" : process.arch === "arm64" ? "aarch64_linux" : "x86_64_linux";
|
|
@@ -409,7 +436,7 @@ function buildAuthHeaders(body, requestURL, creds) {
|
|
|
409
436
|
"Cosy-Machinetoken": machineID,
|
|
410
437
|
"Cosy-Machinetype": qoderMachineTypeMagic,
|
|
411
438
|
"Cosy-Machineos": qoderMachineOs,
|
|
412
|
-
"Cosy-Clienttype":
|
|
439
|
+
"Cosy-Clienttype": "5",
|
|
413
440
|
"Cosy-Clientip": "127.0.0.1",
|
|
414
441
|
"Cosy-Bodyhash": bodyHash,
|
|
415
442
|
"Cosy-Bodylength": bodyLen,
|
|
@@ -421,48 +448,418 @@ function buildAuthHeaders(body, requestURL, creds) {
|
|
|
421
448
|
"X-Request-Id": crypto.randomUUID()
|
|
422
449
|
};
|
|
423
450
|
}
|
|
451
|
+
const defaultMaxJsonBytes = 2 * 1024 * 1024;
|
|
452
|
+
const defaultMaxErrorBytes = 16 * 1024;
|
|
453
|
+
const metadataRetryBaseDelayMs = 200;
|
|
454
|
+
const maxProviderRetryDelayMs = 1e4;
|
|
455
|
+
function opaqueCredentialKey(value) {
|
|
456
|
+
return createHash("sha256").update(value).digest("hex");
|
|
457
|
+
}
|
|
458
|
+
function retryableMetadataError(error) {
|
|
459
|
+
return error instanceof QoderLlmError && [
|
|
460
|
+
"RATE_LIMIT",
|
|
461
|
+
"SERVER",
|
|
462
|
+
"TIMEOUT",
|
|
463
|
+
"TRANSPORT"
|
|
464
|
+
].includes(error.code);
|
|
465
|
+
}
|
|
466
|
+
function abortableDelay(ms, signal) {
|
|
467
|
+
if (signal.aborted) return Promise.reject(new DOMException("aborted", "AbortError"));
|
|
468
|
+
return new Promise((resolve, reject) => {
|
|
469
|
+
const timer = setTimeout(() => {
|
|
470
|
+
signal.removeEventListener("abort", onAbort);
|
|
471
|
+
resolve();
|
|
472
|
+
}, ms);
|
|
473
|
+
const onAbort = () => {
|
|
474
|
+
clearTimeout(timer);
|
|
475
|
+
reject(new DOMException("aborted", "AbortError"));
|
|
476
|
+
};
|
|
477
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
async function retryMetadataRead(signal, operation) {
|
|
481
|
+
try {
|
|
482
|
+
return await operation();
|
|
483
|
+
} catch (error) {
|
|
484
|
+
if (!retryableMetadataError(error) || signal.aborted) throw error;
|
|
485
|
+
const providerDelay = error.failure.providerRetryAfterMs;
|
|
486
|
+
await abortableDelay(providerDelay === void 0 ? metadataRetryBaseDelayMs + Math.floor(Math.random() * 41) : Math.min(providerDelay, maxProviderRetryDelayMs), signal);
|
|
487
|
+
return operation();
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function withDeadline(signal, timeoutMs) {
|
|
491
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
492
|
+
return {
|
|
493
|
+
timeoutSignal,
|
|
494
|
+
signal: signal === void 0 ? timeoutSignal : AbortSignal.any([signal, timeoutSignal])
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
async function readLimitedText(response, maxBytes, label) {
|
|
498
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
499
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
|
|
500
|
+
await response.body?.cancel().catch(() => void 0);
|
|
501
|
+
throw new QoderLlmError(`${label} exceeded its response size limit.`, "MALFORMED_RESPONSE");
|
|
502
|
+
}
|
|
503
|
+
if (!response.body) return "";
|
|
504
|
+
const reader = response.body.getReader();
|
|
505
|
+
const decoder = new TextDecoder();
|
|
506
|
+
let bytes = 0;
|
|
507
|
+
let text = "";
|
|
508
|
+
try {
|
|
509
|
+
while (true) {
|
|
510
|
+
const { done, value } = await reader.read();
|
|
511
|
+
if (done) break;
|
|
512
|
+
bytes += value.byteLength;
|
|
513
|
+
if (bytes > maxBytes) throw new QoderLlmError(`${label} exceeded its response size limit.`, "MALFORMED_RESPONSE");
|
|
514
|
+
text += decoder.decode(value, { stream: true });
|
|
515
|
+
}
|
|
516
|
+
return text + decoder.decode();
|
|
517
|
+
} finally {
|
|
518
|
+
await reader.cancel().catch(() => void 0);
|
|
519
|
+
reader.releaseLock();
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
var SingleFlight = class {
|
|
523
|
+
flights = /* @__PURE__ */ new Map();
|
|
524
|
+
run(key, signal, start, abortedError) {
|
|
525
|
+
if (signal?.aborted) return Promise.reject(abortedError());
|
|
526
|
+
let flight = this.flights.get(key);
|
|
527
|
+
if (flight === void 0 || flight.controller.signal.aborted) {
|
|
528
|
+
const controller = new AbortController();
|
|
529
|
+
const created = {};
|
|
530
|
+
created.controller = controller;
|
|
531
|
+
created.settled = false;
|
|
532
|
+
created.waiters = 0;
|
|
533
|
+
created.promise = start(controller.signal).finally(() => {
|
|
534
|
+
created.settled = true;
|
|
535
|
+
if (this.flights.get(key) === created) this.flights.delete(key);
|
|
536
|
+
});
|
|
537
|
+
flight = created;
|
|
538
|
+
this.flights.set(key, created);
|
|
539
|
+
}
|
|
540
|
+
flight.waiters++;
|
|
541
|
+
return new Promise((resolve, reject) => {
|
|
542
|
+
let finished = false;
|
|
543
|
+
const finish = (callback) => {
|
|
544
|
+
if (finished) return;
|
|
545
|
+
finished = true;
|
|
546
|
+
signal?.removeEventListener("abort", onAbort);
|
|
547
|
+
flight.waiters--;
|
|
548
|
+
if (flight.waiters === 0 && !flight.settled) {
|
|
549
|
+
if (this.flights.get(key) === flight) this.flights.delete(key);
|
|
550
|
+
flight.controller.abort("all callers aborted");
|
|
551
|
+
}
|
|
552
|
+
callback();
|
|
553
|
+
};
|
|
554
|
+
const onAbort = () => finish(() => reject(abortedError()));
|
|
555
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
556
|
+
flight.promise.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
async function openApiJsonRequest(fetchImpl, options) {
|
|
561
|
+
const method = options.method ?? (options.body !== void 0 ? "POST" : "GET");
|
|
562
|
+
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
563
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
564
|
+
const requestSignal = options.signal === void 0 ? timeoutSignal : AbortSignal.any([options.signal, timeoutSignal]);
|
|
565
|
+
const startedAt = performance.now();
|
|
566
|
+
options.logger?.debug?.(`[Qoder ${options.operation}] Requesting`, {
|
|
567
|
+
url: options.url,
|
|
568
|
+
method
|
|
569
|
+
});
|
|
570
|
+
try {
|
|
571
|
+
const headers = {
|
|
572
|
+
accept: "application/json",
|
|
573
|
+
"user-agent": options.userAgent ?? defaultUserAgent,
|
|
574
|
+
"cosy-version": "1.0.1",
|
|
575
|
+
"cosy-clienttype": "5",
|
|
576
|
+
...options.headers
|
|
577
|
+
};
|
|
578
|
+
if (options.token) headers.authorization = `Bearer ${options.token}`;
|
|
579
|
+
if (options.machineId) {
|
|
580
|
+
headers["Cosy-MachineToken"] = options.machineId;
|
|
581
|
+
headers["Cosy-MachineType"] = "host";
|
|
582
|
+
}
|
|
583
|
+
let bodyText;
|
|
584
|
+
if (options.body !== void 0) {
|
|
585
|
+
headers["content-type"] = "application/json";
|
|
586
|
+
bodyText = typeof options.body === "string" ? options.body : JSON.stringify(options.body);
|
|
587
|
+
}
|
|
588
|
+
const response = await fetchImpl(options.url, {
|
|
589
|
+
method,
|
|
590
|
+
headers,
|
|
591
|
+
body: bodyText,
|
|
592
|
+
signal: requestSignal
|
|
593
|
+
});
|
|
594
|
+
options.logger?.debug?.(`[Qoder ${options.operation}] Request completed`, {
|
|
595
|
+
status: response.status,
|
|
596
|
+
statusText: response.statusText,
|
|
597
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
598
|
+
...qoderRequestId(response.headers) === void 0 ? {} : { requestId: qoderRequestId(response.headers) }
|
|
599
|
+
});
|
|
600
|
+
const text = await readLimitedText(response, response.ok ? defaultMaxJsonBytes : defaultMaxErrorBytes, `Qoder ${options.operation} response`);
|
|
601
|
+
if (!response.ok) {
|
|
602
|
+
options.logger?.error?.(`[Qoder ${options.operation}] Request failed`, redactLogPayload(text));
|
|
603
|
+
throw qoderHttpError(`Failed to execute Qoder ${options.operation} with status ${response.status}.`, response);
|
|
604
|
+
}
|
|
605
|
+
let data;
|
|
606
|
+
try {
|
|
607
|
+
data = JSON.parse(text);
|
|
608
|
+
} catch {
|
|
609
|
+
options.logger?.error?.(`[Qoder ${options.operation}] Invalid JSON response`, redactLogPayload(text));
|
|
610
|
+
throw new QoderLlmError(`Failed to parse Qoder ${options.operation} JSON response`, "MALFORMED_RESPONSE");
|
|
611
|
+
}
|
|
612
|
+
if (options.logCategory) logParsedResponse(options.logger, options.logCategory, data);
|
|
613
|
+
return data;
|
|
614
|
+
} catch (error) {
|
|
615
|
+
if (error instanceof QoderLlmError) throw error;
|
|
616
|
+
if (options.signal?.aborted) throw new QoderLlmError(`Qoder ${options.operation} request was aborted.`, "ABORTED");
|
|
617
|
+
if (timeoutSignal.aborted) throw new QoderLlmError(`Qoder ${options.operation} request timed out.`, "TIMEOUT");
|
|
618
|
+
throw new QoderLlmError(`Qoder ${options.operation} network request failed.`, "TRANSPORT", { cause: error });
|
|
619
|
+
}
|
|
620
|
+
}
|
|
424
621
|
//#endregion
|
|
425
|
-
//#region src/
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
622
|
+
//#region src/qoder/transport/auth.ts
|
|
623
|
+
const expiryBufferMs = 300 * 1e3;
|
|
624
|
+
const defaultExpiryMs = 1440 * 60 * 1e3;
|
|
625
|
+
const defaultAuthTimeoutMs = 15e3;
|
|
626
|
+
function abortedError() {
|
|
627
|
+
return new QoderLlmError("Qoder authentication was aborted.", "ABORTED");
|
|
628
|
+
}
|
|
629
|
+
async function waitForFlight(promise, signal) {
|
|
630
|
+
if (signal === void 0) return promise;
|
|
631
|
+
if (signal.aborted) throw abortedError();
|
|
632
|
+
return new Promise((resolve, reject) => {
|
|
633
|
+
const onAbort = () => reject(abortedError());
|
|
634
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
635
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
var QoderAuthService = class {
|
|
639
|
+
cache = /* @__PURE__ */ new Map();
|
|
640
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
641
|
+
fetchImpl;
|
|
642
|
+
timeoutMs;
|
|
643
|
+
resolveMachineId;
|
|
644
|
+
region;
|
|
645
|
+
logger;
|
|
646
|
+
constructor(options = {}) {
|
|
647
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
648
|
+
this.timeoutMs = options.timeoutMs ?? defaultAuthTimeoutMs;
|
|
649
|
+
this.resolveMachineId = options.resolveMachineId ?? getMachineId;
|
|
650
|
+
this.region = options.region ?? "global";
|
|
651
|
+
this.logger = options.logger;
|
|
652
|
+
}
|
|
653
|
+
clear(pat) {
|
|
654
|
+
if (pat) this.cache.delete(`${this.region}:${opaqueCredentialKey(pat)}`);
|
|
655
|
+
else this.cache.clear();
|
|
656
|
+
}
|
|
657
|
+
async getCredentials(pat, signal) {
|
|
658
|
+
if (!pat || typeof pat !== "string") throw new QoderLlmError("Qoder Personal Access Token is missing or invalid. Configure Qoder in the Qoder settings page.", "MISSING_CREDENTIAL");
|
|
659
|
+
if (signal?.aborted) throw abortedError();
|
|
660
|
+
const cacheKey = `${this.region}:${opaqueCredentialKey(pat)}`;
|
|
661
|
+
const cached = this.cache.get(cacheKey);
|
|
662
|
+
if (cached && cached.expiresAt > Date.now() + expiryBufferMs) return cached.creds;
|
|
663
|
+
let entry = this.inFlight.get(cacheKey);
|
|
664
|
+
if (entry === void 0 || entry.controller.signal.aborted) {
|
|
665
|
+
const controller = new AbortController();
|
|
666
|
+
const created = {};
|
|
667
|
+
created.controller = controller;
|
|
668
|
+
created.waiters = 0;
|
|
669
|
+
created.settled = false;
|
|
670
|
+
created.timeout = setTimeout(() => controller.abort("authentication timeout"), this.timeoutMs);
|
|
671
|
+
created.promise = this.exchangeAndResolve(pat, controller.signal).finally(() => {
|
|
672
|
+
created.settled = true;
|
|
673
|
+
clearTimeout(created.timeout);
|
|
674
|
+
if (this.inFlight.get(cacheKey) === created) this.inFlight.delete(cacheKey);
|
|
675
|
+
});
|
|
676
|
+
entry = created;
|
|
677
|
+
this.inFlight.set(cacheKey, entry);
|
|
678
|
+
}
|
|
679
|
+
entry.waiters++;
|
|
680
|
+
try {
|
|
681
|
+
return await waitForFlight(entry.promise, signal);
|
|
682
|
+
} finally {
|
|
683
|
+
entry.waiters--;
|
|
684
|
+
if (entry.waiters === 0 && !entry.settled) {
|
|
685
|
+
if (this.inFlight.get(cacheKey) === entry) this.inFlight.delete(cacheKey);
|
|
686
|
+
entry.controller.abort("all callers aborted");
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
async exchangeAndResolve(pat, signal) {
|
|
691
|
+
let jobToken;
|
|
692
|
+
let expiresAt = Date.now() + defaultExpiryMs;
|
|
693
|
+
const data = await openApiJsonRequest(this.fetchImpl, {
|
|
694
|
+
url: getQoderExchangeUrl(this.region),
|
|
695
|
+
body: { personal_token: pat },
|
|
696
|
+
signal,
|
|
697
|
+
timeoutMs: this.timeoutMs,
|
|
698
|
+
logger: this.logger,
|
|
699
|
+
operation: "Auth",
|
|
700
|
+
logCategory: "auth.exchange"
|
|
701
|
+
});
|
|
702
|
+
if (!data.token) throw new QoderLlmError("Qoder PAT exchange returned no job token.", "AUTH");
|
|
703
|
+
jobToken = data.token;
|
|
704
|
+
if (data.expires_at) {
|
|
705
|
+
const parsed = Date.parse(data.expires_at);
|
|
706
|
+
if (!Number.isNaN(parsed)) expiresAt = parsed;
|
|
707
|
+
} else if (typeof data.expires_in === "number" && data.expires_in > 0) expiresAt = Date.now() + data.expires_in;
|
|
708
|
+
const userInfo = await retryMetadataRead(signal, () => this.fetchUserInfo(jobToken, signal));
|
|
709
|
+
const creds = {
|
|
710
|
+
userID: userInfo.userID,
|
|
711
|
+
authToken: jobToken,
|
|
712
|
+
name: userInfo.name || "Qoder User",
|
|
713
|
+
email: userInfo.email,
|
|
714
|
+
machineID: this.resolveMachineId()
|
|
715
|
+
};
|
|
716
|
+
const cacheKey = `${this.region}:${opaqueCredentialKey(pat)}`;
|
|
717
|
+
this.cache.set(cacheKey, {
|
|
718
|
+
creds,
|
|
719
|
+
expiresAt
|
|
720
|
+
});
|
|
721
|
+
return creds;
|
|
722
|
+
}
|
|
723
|
+
async fetchUserInfo(jobToken, signal) {
|
|
724
|
+
const info = await openApiJsonRequest(this.fetchImpl, {
|
|
725
|
+
url: getQoderUserInfoUrl(this.region),
|
|
726
|
+
token: jobToken,
|
|
727
|
+
signal,
|
|
728
|
+
timeoutMs: this.timeoutMs,
|
|
729
|
+
logger: this.logger,
|
|
730
|
+
operation: "UserInfo",
|
|
731
|
+
logCategory: "auth.user-info"
|
|
732
|
+
});
|
|
733
|
+
if (!info.id) throw new QoderLlmError("Qoder identity lookup returned no user id.", "AUTH");
|
|
734
|
+
return {
|
|
735
|
+
userID: info.id,
|
|
736
|
+
email: info.email ?? "",
|
|
737
|
+
name: info.name ?? info.username ?? ""
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
//#endregion
|
|
742
|
+
//#region src/qoder/transport/catalog-reader.ts
|
|
743
|
+
async function fetchQoderModels(credentials, options = {}) {
|
|
744
|
+
const url = getQoderModelListUrl(options.region);
|
|
745
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
746
|
+
const startedAt = performance.now();
|
|
747
|
+
const deadline = withDeadline(options.signal, options.timeoutMs ?? 15e3);
|
|
748
|
+
options.logger?.debug?.("[Qoder Models] Requesting model catalog", { url });
|
|
749
|
+
try {
|
|
750
|
+
const response = await fetchImpl(url, {
|
|
751
|
+
method: "GET",
|
|
752
|
+
headers: {
|
|
753
|
+
accept: "application/json",
|
|
754
|
+
...buildAuthHeaders(null, url, credentials)
|
|
755
|
+
},
|
|
756
|
+
signal: deadline.signal
|
|
757
|
+
});
|
|
758
|
+
options.logger?.debug?.("[Qoder Models] Catalog request completed", {
|
|
759
|
+
url,
|
|
760
|
+
status: response.status,
|
|
761
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
762
|
+
...qoderRequestId(response.headers) === void 0 ? {} : { requestId: qoderRequestId(response.headers) }
|
|
763
|
+
});
|
|
764
|
+
const text = await readLimitedText(response, response.ok ? defaultMaxJsonBytes : defaultMaxErrorBytes, "Qoder model discovery response");
|
|
765
|
+
if (!response.ok) {
|
|
766
|
+
options.logger?.error?.("[Qoder Models] Catalog request failed", redactLogPayload(text));
|
|
767
|
+
throw qoderHttpError(`Qoder model discovery failed with HTTP status ${response.status}.`, response);
|
|
768
|
+
}
|
|
769
|
+
let payload;
|
|
770
|
+
try {
|
|
771
|
+
payload = JSON.parse(text);
|
|
772
|
+
} catch {
|
|
773
|
+
throw new QoderLlmError("Qoder model discovery returned invalid JSON.", "MALFORMED_RESPONSE");
|
|
449
774
|
}
|
|
775
|
+
const models = normalizeQoderModels(payload, (conflict) => {
|
|
776
|
+
options.logger?.warn?.("[Qoder Models] Conflicting catalog defaults; using fallback", { conflict });
|
|
777
|
+
});
|
|
778
|
+
if (models.length === 0) throw new QoderLlmError("Qoder model discovery returned no enabled models.", "EMPTY_RESPONSE");
|
|
779
|
+
return models;
|
|
780
|
+
} catch (error) {
|
|
781
|
+
if (error instanceof QoderLlmError) throw error;
|
|
782
|
+
if (options.signal?.aborted) throw new QoderLlmError("Qoder model discovery was aborted.", "ABORTED");
|
|
783
|
+
if (deadline.timeoutSignal.aborted) throw new QoderLlmError("Qoder model discovery timed out.", "TIMEOUT");
|
|
784
|
+
options.logger?.error?.("[Qoder Models] Catalog network request failed", redactLogValue(error));
|
|
785
|
+
throw new QoderLlmError("Qoder model discovery network request failed.", "TRANSPORT", { cause: error });
|
|
450
786
|
}
|
|
451
|
-
return out;
|
|
452
787
|
}
|
|
453
788
|
//#endregion
|
|
454
|
-
//#region src/translate.ts
|
|
789
|
+
//#region src/qoder/transport/wire/translate.ts
|
|
455
790
|
function unsupported(message) {
|
|
456
791
|
return new QoderLlmError(message, "UNSUPPORTED_CONTENT");
|
|
457
792
|
}
|
|
458
793
|
function toolResultText(block) {
|
|
459
794
|
let text = "";
|
|
460
795
|
for (const nested of block.content) {
|
|
796
|
+
if (nested.type === "image") continue;
|
|
461
797
|
if (nested.type !== "text") throw unsupported(`Qoder tool results support text only; received nested ${String(nested.type)} content.`);
|
|
462
798
|
text += nested.text;
|
|
463
799
|
}
|
|
464
800
|
return text;
|
|
465
801
|
}
|
|
802
|
+
/**
|
|
803
|
+
* Check message shapes without performing any provider I/O.
|
|
804
|
+
*
|
|
805
|
+
* Callers run this before resolving credentials so an invalid request never
|
|
806
|
+
* consumes a Qoder subscription.
|
|
807
|
+
*/
|
|
808
|
+
function validateMessageShapes(messages) {
|
|
809
|
+
for (const message of messages) {
|
|
810
|
+
const toolResults = message.content.filter((block) => block.type === "tool-result");
|
|
811
|
+
if (toolResults.length > 0) {
|
|
812
|
+
if (message.role !== "user" || toolResults.length !== message.content.length) throw unsupported("Qoder tool-result messages cannot contain sibling content or use a non-user role.");
|
|
813
|
+
for (const result of toolResults) toolResultText(result);
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
for (const block of message.content) {
|
|
817
|
+
if (block.type === "text") continue;
|
|
818
|
+
if (block.type === "image") {
|
|
819
|
+
if (message.role !== "user") throw unsupported("Qoder image content is valid only in user messages.");
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
if (block.type === "tool-call") {
|
|
823
|
+
if (message.role !== "assistant") throw unsupported("Qoder tool calls are valid only in assistant messages.");
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
if (block.type === "reasoning") continue;
|
|
827
|
+
throw unsupported(`Qoder transport encountered unsupported block type: ${String(block.type)}`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
/** Reject a batch that exceeds the deployment image policy before any upload work starts. */
|
|
832
|
+
function enforceImageLimits(images, attachments) {
|
|
833
|
+
const limits = attachments.imageLimits;
|
|
834
|
+
if (images.length > limits.maxImagesPerMessage) throw unsupported(`Qoder accepts at most ${limits.maxImagesPerMessage} images per message; received ${images.length}.`);
|
|
835
|
+
let total = 0;
|
|
836
|
+
for (const image of images) total += image.attachment.bytes;
|
|
837
|
+
if (total > limits.maxMessageImageBytes) throw unsupported("Qoder message image content exceeds the configured total byte limit.");
|
|
838
|
+
}
|
|
839
|
+
async function resolveImagePart(block, context) {
|
|
840
|
+
const { attachments, uploader, credentials, signal } = context;
|
|
841
|
+
if (attachments === void 0) throw new QoderLlmError("Qoder image input requires the DSH attachment service.", "ATTACHMENT");
|
|
842
|
+
let image;
|
|
843
|
+
try {
|
|
844
|
+
const limits = attachments.imageLimits;
|
|
845
|
+
image = await attachments.readImageRequest(block.attachment, {
|
|
846
|
+
maxPixels: limits.maxImagePixels,
|
|
847
|
+
maxBytes: limits.maxImageBytes
|
|
848
|
+
}, signal);
|
|
849
|
+
} catch (error) {
|
|
850
|
+
if (signal?.aborted) throw new QoderLlmError("Qoder image preparation was aborted.", "ABORTED", { cause: error });
|
|
851
|
+
if (error instanceof QoderLlmError) throw error;
|
|
852
|
+
throw new QoderLlmError("Qoder could not prepare an image attachment.", "ATTACHMENT", { cause: error });
|
|
853
|
+
}
|
|
854
|
+
if (uploader !== void 0 && credentials !== void 0) return {
|
|
855
|
+
type: "image_url",
|
|
856
|
+
image_url: { url: await uploader.resolveImageUrl(image, credentials, signal) }
|
|
857
|
+
};
|
|
858
|
+
return {
|
|
859
|
+
type: "image_url",
|
|
860
|
+
image_url: { url: `data:${image.mediaType};base64,${Buffer.from(image.data).toString("base64")}` }
|
|
861
|
+
};
|
|
862
|
+
}
|
|
466
863
|
function translateTools(tools) {
|
|
467
864
|
return (tools ?? []).map((tool) => ({
|
|
468
865
|
type: "function",
|
|
@@ -473,7 +870,15 @@ function translateTools(tools) {
|
|
|
473
870
|
}
|
|
474
871
|
}));
|
|
475
872
|
}
|
|
476
|
-
function validateAndTranslateMessages(messages, systemPrompt) {
|
|
873
|
+
async function validateAndTranslateMessages(messages, systemPrompt, attachments, signal, pipeline) {
|
|
874
|
+
validateMessageShapes(messages);
|
|
875
|
+
const context = {
|
|
876
|
+
attachments,
|
|
877
|
+
signal,
|
|
878
|
+
uploader: pipeline?.uploader,
|
|
879
|
+
credentials: pipeline?.credentials
|
|
880
|
+
};
|
|
881
|
+
const preserveThinking = pipeline?.preserveThinking ?? true;
|
|
477
882
|
const output = [];
|
|
478
883
|
if (typeof systemPrompt === "string" && systemPrompt.trim().length > 0) output.push({
|
|
479
884
|
role: "system",
|
|
@@ -483,23 +888,51 @@ function validateAndTranslateMessages(messages, systemPrompt) {
|
|
|
483
888
|
const toolResults = message.content.filter((block) => block.type === "tool-result");
|
|
484
889
|
if (toolResults.length > 0) {
|
|
485
890
|
if (message.role !== "user" || toolResults.length !== message.content.length) throw unsupported("Qoder tool-result messages cannot contain sibling content or use a non-user role.");
|
|
486
|
-
for (const result of toolResults)
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
891
|
+
for (const result of toolResults) {
|
|
892
|
+
output.push({
|
|
893
|
+
role: "tool",
|
|
894
|
+
tool_call_id: String(result.toolCallId),
|
|
895
|
+
content: toolResultText(result)
|
|
896
|
+
});
|
|
897
|
+
const images = result.content.filter((block) => block.type === "image");
|
|
898
|
+
if (images.length > 0) {
|
|
899
|
+
if (attachments !== void 0) enforceImageLimits(images, attachments);
|
|
900
|
+
output.push({
|
|
901
|
+
role: "user",
|
|
902
|
+
content: [{
|
|
903
|
+
type: "text",
|
|
904
|
+
text: `[${images.length} image${images.length === 1 ? "" : "s"} returned by the previous tool call]`
|
|
905
|
+
}, ...await Promise.all(images.map((image) => resolveImagePart(image, context)))]
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
}
|
|
491
909
|
continue;
|
|
492
910
|
}
|
|
493
911
|
let text = "";
|
|
912
|
+
let reasoningText = "";
|
|
913
|
+
const userContent = [];
|
|
914
|
+
const pendingImages = [];
|
|
915
|
+
let hasImage = false;
|
|
494
916
|
const toolCalls = [];
|
|
495
917
|
for (const block of message.content) {
|
|
496
918
|
if (block.type === "text") {
|
|
497
919
|
text += block.text;
|
|
920
|
+
if (message.role === "user") userContent.push({
|
|
921
|
+
type: "text",
|
|
922
|
+
text: block.text
|
|
923
|
+
});
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
if (block.type === "image") {
|
|
927
|
+
hasImage = true;
|
|
928
|
+
pendingImages.push({
|
|
929
|
+
slot: userContent.length,
|
|
930
|
+
block
|
|
931
|
+
});
|
|
932
|
+
userContent.push(void 0);
|
|
498
933
|
continue;
|
|
499
934
|
}
|
|
500
|
-
if (block.type === "image") throw unsupported("Qoder transport does not support image content.");
|
|
501
935
|
if (block.type === "tool-call") {
|
|
502
|
-
if (message.role !== "assistant") throw unsupported("Qoder tool calls are valid only in assistant messages.");
|
|
503
936
|
toolCalls.push({
|
|
504
937
|
id: String(block.id),
|
|
505
938
|
type: "function",
|
|
@@ -510,31 +943,37 @@ function validateAndTranslateMessages(messages, systemPrompt) {
|
|
|
510
943
|
});
|
|
511
944
|
continue;
|
|
512
945
|
}
|
|
513
|
-
if (block.type === "tool-result") throw unsupported("Qoder encountered an invalid mixed tool-result message.");
|
|
514
946
|
if (block.type === "reasoning") {
|
|
515
|
-
if (message.role
|
|
947
|
+
if (message.role === "assistant") reasoningText += block.text;
|
|
516
948
|
continue;
|
|
517
949
|
}
|
|
518
|
-
throw unsupported(`Qoder transport encountered unsupported block type: ${String(block.type)}`);
|
|
519
950
|
}
|
|
520
951
|
if (message.role === "assistant") {
|
|
521
|
-
|
|
952
|
+
const hasReasoning = preserveThinking && reasoningText.length > 0;
|
|
953
|
+
if (!text && toolCalls.length === 0 && !hasReasoning) continue;
|
|
522
954
|
output.push({
|
|
523
955
|
role: "assistant",
|
|
524
956
|
content: text || " ",
|
|
525
|
-
...toolCalls.length === 0 ? {} : { tool_calls: toolCalls }
|
|
957
|
+
...toolCalls.length === 0 ? {} : { tool_calls: toolCalls },
|
|
958
|
+
...hasReasoning ? { reasoning_content: reasoningText } : {}
|
|
526
959
|
});
|
|
527
960
|
continue;
|
|
528
961
|
}
|
|
962
|
+
if (pendingImages.length > 0) {
|
|
963
|
+
if (attachments !== void 0) enforceImageLimits(pendingImages.map((pending) => pending.block), attachments);
|
|
964
|
+
await Promise.all(pendingImages.map(async (pending) => {
|
|
965
|
+
userContent[pending.slot] = await resolveImagePart(pending.block, context);
|
|
966
|
+
}));
|
|
967
|
+
}
|
|
529
968
|
output.push({
|
|
530
969
|
role: message.role,
|
|
531
|
-
content: text
|
|
970
|
+
content: hasImage ? userContent.filter((part) => part !== void 0) : text
|
|
532
971
|
});
|
|
533
972
|
}
|
|
534
973
|
return output;
|
|
535
974
|
}
|
|
536
975
|
//#endregion
|
|
537
|
-
//#region src/serialize.ts
|
|
976
|
+
//#region src/qoder/transport/wire/serialize.ts
|
|
538
977
|
/** Build the minimum qodercli request envelope for a validated DSH request. */
|
|
539
978
|
function stableHash(prefix, ...inputs) {
|
|
540
979
|
const hash = crypto.createHash("sha256");
|
|
@@ -558,28 +997,46 @@ function stableChatRecordId(model, messages, tools, maxTokens) {
|
|
|
558
997
|
hash.update(`mt=${maxTokens}`);
|
|
559
998
|
return hash.digest("hex").slice(0, 16);
|
|
560
999
|
}
|
|
561
|
-
|
|
1000
|
+
/**
|
|
1001
|
+
* Reject an unusable request before any credential resolution or provider I/O.
|
|
1002
|
+
*
|
|
1003
|
+
* Image publication needs credentials, so message translation now runs after
|
|
1004
|
+
* authentication. This static pass preserves the guarantee that a request the
|
|
1005
|
+
* provider cannot serve never reaches the network.
|
|
1006
|
+
*/
|
|
1007
|
+
function validateQoderRequestShape(options, model) {
|
|
562
1008
|
if (options.reasoningEffort !== void 0) {
|
|
563
1009
|
const effort = String(options.reasoningEffort);
|
|
564
1010
|
if (!model?.reasoningEfforts?.some((candidate) => candidate.id === effort)) throw new QoderLlmError(`Qoder model "${options.model}" does not advertise reasoning effort "${effort}".`, "UNSUPPORTED_REASONING_EFFORT");
|
|
565
1011
|
}
|
|
566
|
-
|
|
1012
|
+
if (options.messages.some((message) => contentHasImage(message.content)) && model?.supportsImages !== true) throw new QoderLlmError(`Qoder model "${options.model}" does not advertise image input.`, "UNSUPPORTED_CONTENT");
|
|
1013
|
+
validateMessageShapes(options.messages);
|
|
1014
|
+
}
|
|
1015
|
+
/** Translate a request whose shape has already been validated. */
|
|
1016
|
+
function translateQoderMessages(options, attachments, pipeline) {
|
|
1017
|
+
return validateAndTranslateMessages(options.messages, options.system, attachments, options.signal, pipeline);
|
|
1018
|
+
}
|
|
1019
|
+
async function validateQoderRequest(options, model, attachments) {
|
|
1020
|
+
validateQoderRequestShape(options, model);
|
|
1021
|
+
return validateAndTranslateMessages(options.messages, options.system, attachments, options.signal);
|
|
567
1022
|
}
|
|
568
|
-
function buildQoderRequestBody(options, userId, translatedMessages, model) {
|
|
1023
|
+
async function buildQoderRequestBody(options, userId, translatedMessages, model, attachments) {
|
|
569
1024
|
if (!userId) throw new QoderLlmError("Qoder request identity is missing.", "AUTH");
|
|
570
1025
|
const modelKey = options.model || "cmodel";
|
|
571
|
-
const messages = translatedMessages ?? validateQoderRequest(options, model);
|
|
1026
|
+
const messages = translatedMessages ?? await validateQoderRequest(options, model, attachments);
|
|
572
1027
|
const modelMaxTokens = model?.maxTokens ?? 32768;
|
|
573
1028
|
const maxTokens = Math.min(options.maxTokens ?? modelMaxTokens, modelMaxTokens);
|
|
574
|
-
const isReasoning = model?.isReasoning ?? false;
|
|
1029
|
+
const isReasoning = options.reasoningEffort !== void 0 || (model?.isReasoning ?? false);
|
|
575
1030
|
const tools = translateTools(options.tools);
|
|
576
|
-
const
|
|
1031
|
+
const defaultContexts = Object.values(model?.contextOptions ?? {}).filter((option) => option.isDefault === true && typeof option.tokenCount === "number" && Number.isFinite(option.tokenCount) && option.tokenCount > 0);
|
|
1032
|
+
const contextConfig = model?.contextOptions === void 0 || defaultContexts.length !== 1 ? void 0 : Object.fromEntries(Object.entries(model.contextOptions).map(([key, value]) => [key, {
|
|
577
1033
|
...value.tokenCount === void 0 ? {} : { token_count: value.tokenCount },
|
|
578
1034
|
...value.isDefault === void 0 ? {} : { is_default: value.isDefault }
|
|
579
1035
|
}]));
|
|
580
1036
|
let lastUserText = "";
|
|
581
1037
|
for (let index = messages.length - 1; index >= 0; index--) if (messages[index].role === "user") {
|
|
582
|
-
|
|
1038
|
+
const content = messages[index].content;
|
|
1039
|
+
lastUserText = typeof content === "string" ? content : Array.isArray(content) ? content.filter((part) => part.type === "text").map((part) => part.text).join("") : "";
|
|
583
1040
|
break;
|
|
584
1041
|
}
|
|
585
1042
|
const stablePart = stableHash("qoder-session", userId, modelKey);
|
|
@@ -640,10 +1097,570 @@ function buildQoderRequestBody(options, userId, translatedMessages, model) {
|
|
|
640
1097
|
name: lastUserText.substring(0, 30),
|
|
641
1098
|
begin_at: Date.now()
|
|
642
1099
|
}
|
|
643
|
-
};
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
const mediaTypeExtensions = {
|
|
1103
|
+
"image/png": "png",
|
|
1104
|
+
"image/jpeg": "jpg",
|
|
1105
|
+
"image/webp": "webp",
|
|
1106
|
+
"image/gif": "gif"
|
|
1107
|
+
};
|
|
1108
|
+
function aborted$2() {
|
|
1109
|
+
return new QoderLlmError("Qoder image upload was aborted.", "ABORTED");
|
|
1110
|
+
}
|
|
1111
|
+
/** Encode one image as a single-field multipart payload using the Qoder boundary shape. */
|
|
1112
|
+
function buildQoderImageMultipart(data, mediaType, boundaryId = crypto.randomUUID()) {
|
|
1113
|
+
const boundary = `----qodercli-${boundaryId}`;
|
|
1114
|
+
const extension = mediaTypeExtensions[mediaType] ?? "png";
|
|
1115
|
+
const header = Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="image.${extension}"\r\nContent-Type: ${mediaType}\r\n\r\n`, "utf8");
|
|
1116
|
+
const footer = Buffer.from(`\r\n--${boundary}--\r\n`, "utf8");
|
|
1117
|
+
return {
|
|
1118
|
+
body: Buffer.concat([
|
|
1119
|
+
header,
|
|
1120
|
+
Buffer.from(data),
|
|
1121
|
+
footer
|
|
1122
|
+
]),
|
|
1123
|
+
boundary
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Extract the durable object URL from a center upload response.
|
|
1128
|
+
*
|
|
1129
|
+
* @param payload - parsed response body.
|
|
1130
|
+
* @returns the first usable absolute URL, or undefined when none is present.
|
|
1131
|
+
*/
|
|
1132
|
+
function readQoderImageUrl(payload) {
|
|
1133
|
+
if (payload === null || typeof payload !== "object") return void 0;
|
|
1134
|
+
const root = payload;
|
|
1135
|
+
const result = root.result;
|
|
1136
|
+
const data = root.data;
|
|
1137
|
+
const candidates = [
|
|
1138
|
+
root.url,
|
|
1139
|
+
result?.url,
|
|
1140
|
+
result?.oss_url,
|
|
1141
|
+
data?.url,
|
|
1142
|
+
data?.oss_url
|
|
1143
|
+
];
|
|
1144
|
+
for (const candidate of candidates) {
|
|
1145
|
+
if (typeof candidate !== "string") continue;
|
|
1146
|
+
const trimmed = candidate.trim();
|
|
1147
|
+
if (trimmed.length === 0) continue;
|
|
1148
|
+
if (!URL.canParse(trimmed)) continue;
|
|
1149
|
+
return trimmed;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
function dataUrl(image) {
|
|
1153
|
+
return `data:${image.mediaType};base64,${Buffer.from(image.data).toString("base64")}`;
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Publishes request images to the Qoder center service and remembers the result.
|
|
1157
|
+
*
|
|
1158
|
+
* Publication never fails a model request: any upstream problem degrades to an
|
|
1159
|
+
* inline data URL so the turn proceeds with the same content it would have
|
|
1160
|
+
* carried before the pipeline existed.
|
|
1161
|
+
*/
|
|
1162
|
+
var QoderImageUploader = class {
|
|
1163
|
+
cache = /* @__PURE__ */ new Map();
|
|
1164
|
+
flights = new SingleFlight();
|
|
1165
|
+
fetchImpl;
|
|
1166
|
+
region;
|
|
1167
|
+
logger;
|
|
1168
|
+
timeoutMs;
|
|
1169
|
+
cacheTtlMs;
|
|
1170
|
+
cacheCapacity;
|
|
1171
|
+
maxConcurrency;
|
|
1172
|
+
refreshCredentials;
|
|
1173
|
+
now;
|
|
1174
|
+
active = 0;
|
|
1175
|
+
queue = [];
|
|
1176
|
+
constructor(options = {}) {
|
|
1177
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1178
|
+
this.region = options.region ?? "global";
|
|
1179
|
+
this.logger = options.logger;
|
|
1180
|
+
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
1181
|
+
this.cacheTtlMs = options.cacheTtlMs ?? 18e5;
|
|
1182
|
+
this.cacheCapacity = options.cacheCapacity ?? 512;
|
|
1183
|
+
this.maxConcurrency = options.maxConcurrency ?? 4;
|
|
1184
|
+
this.refreshCredentials = options.refreshCredentials;
|
|
1185
|
+
this.now = options.now ?? Date.now;
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Resolve the wire URL for one request image.
|
|
1189
|
+
*
|
|
1190
|
+
* @param image - request-encoded image produced by the DSH attachment store.
|
|
1191
|
+
* @param credentials - credentials authorizing the center exchange.
|
|
1192
|
+
* @param signal - optional caller cancellation.
|
|
1193
|
+
* @returns a center object URL, or an inline data URL when publication fails.
|
|
1194
|
+
* @throws QoderLlmError with code `ABORTED` only when the caller cancels.
|
|
1195
|
+
*/
|
|
1196
|
+
async resolveImageUrl(image, credentials, signal) {
|
|
1197
|
+
if (signal?.aborted) throw aborted$2();
|
|
1198
|
+
const key = this.cacheKey(image, credentials);
|
|
1199
|
+
const cached = this.cache.get(key);
|
|
1200
|
+
if (cached !== void 0) {
|
|
1201
|
+
if (cached.expiresAt > this.now()) {
|
|
1202
|
+
this.cache.delete(key);
|
|
1203
|
+
this.cache.set(key, cached);
|
|
1204
|
+
this.logger?.debug?.("[image-upload] cache hit", {
|
|
1205
|
+
url: cached.url,
|
|
1206
|
+
region: this.region,
|
|
1207
|
+
mediaType: image.mediaType,
|
|
1208
|
+
bytes: image.data.byteLength
|
|
1209
|
+
});
|
|
1210
|
+
return cached.url;
|
|
1211
|
+
}
|
|
1212
|
+
this.cache.delete(key);
|
|
1213
|
+
}
|
|
1214
|
+
return this.flights.run(key, signal, async (sharedSignal) => {
|
|
1215
|
+
const url = await this.publish(image, credentials, sharedSignal);
|
|
1216
|
+
if (url !== void 0) {
|
|
1217
|
+
this.remember(key, url);
|
|
1218
|
+
return url;
|
|
1219
|
+
}
|
|
1220
|
+
return dataUrl(image);
|
|
1221
|
+
}, aborted$2);
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Derive the deduplication identity for one request image.
|
|
1225
|
+
*
|
|
1226
|
+
* `variantId` is the attachment store's deterministic cache and upload index
|
|
1227
|
+
* over the attachment, the request policy, and the encoder parameters, so it
|
|
1228
|
+
* identifies these exact bytes without rehashing them. Region and subscriber
|
|
1229
|
+
* are included because a center object is never shared across either.
|
|
1230
|
+
*/
|
|
1231
|
+
cacheKey(image, credentials) {
|
|
1232
|
+
const hash = crypto.createHash("sha256");
|
|
1233
|
+
hash.update(getQoderImageUploadUrl(this.region));
|
|
1234
|
+
hash.update("\0");
|
|
1235
|
+
hash.update(credentials.userID);
|
|
1236
|
+
hash.update("\0");
|
|
1237
|
+
hash.update(image.mediaType);
|
|
1238
|
+
hash.update("\0");
|
|
1239
|
+
const variantId = typeof image.variantId === "string" ? image.variantId : "";
|
|
1240
|
+
if (variantId.length > 0) hash.update(variantId);
|
|
1241
|
+
else hash.update(crypto.createHash("sha256").update(Buffer.from(image.data)).digest("hex"));
|
|
1242
|
+
return hash.digest("hex");
|
|
1243
|
+
}
|
|
1244
|
+
remember(key, url) {
|
|
1245
|
+
this.cache.delete(key);
|
|
1246
|
+
this.cache.set(key, {
|
|
1247
|
+
url,
|
|
1248
|
+
expiresAt: this.now() + this.cacheTtlMs
|
|
1249
|
+
});
|
|
1250
|
+
while (this.cache.size > this.cacheCapacity) {
|
|
1251
|
+
const oldest = this.cache.keys().next();
|
|
1252
|
+
if (oldest.done === true) break;
|
|
1253
|
+
this.cache.delete(oldest.value);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
async withSlot(signal, operation) {
|
|
1257
|
+
if (signal.aborted) throw aborted$2();
|
|
1258
|
+
if (this.active >= this.maxConcurrency) {
|
|
1259
|
+
this.logger?.debug?.("[image-upload] queued", {
|
|
1260
|
+
region: this.region,
|
|
1261
|
+
active: this.active,
|
|
1262
|
+
queued: this.queue.length + 1
|
|
1263
|
+
});
|
|
1264
|
+
await new Promise((resolve, reject) => {
|
|
1265
|
+
const onAbort = () => {
|
|
1266
|
+
const index = this.queue.indexOf(release);
|
|
1267
|
+
if (index !== -1) this.queue.splice(index, 1);
|
|
1268
|
+
reject(aborted$2());
|
|
1269
|
+
};
|
|
1270
|
+
const release = () => {
|
|
1271
|
+
signal.removeEventListener("abort", onAbort);
|
|
1272
|
+
resolve();
|
|
1273
|
+
};
|
|
1274
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1275
|
+
this.queue.push(release);
|
|
1276
|
+
});
|
|
1277
|
+
} else this.active++;
|
|
1278
|
+
try {
|
|
1279
|
+
if (signal.aborted) throw aborted$2();
|
|
1280
|
+
return await operation();
|
|
1281
|
+
} finally {
|
|
1282
|
+
const next = this.queue.shift();
|
|
1283
|
+
if (next !== void 0) next();
|
|
1284
|
+
else this.active--;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
/** Attempt publication, returning undefined when the request must degrade. */
|
|
1288
|
+
async publish(image, credentials, signal) {
|
|
1289
|
+
return this.withSlot(signal, async () => {
|
|
1290
|
+
const first = await this.attempt(image, credentials, signal);
|
|
1291
|
+
if (first.url !== void 0) return first.url;
|
|
1292
|
+
if (!first.retryable || this.refreshCredentials === void 0) {
|
|
1293
|
+
this.warnDegraded(first.reason);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
let refreshed;
|
|
1297
|
+
this.logger?.debug?.("[image-upload] refreshing credentials before retry", {
|
|
1298
|
+
region: this.region,
|
|
1299
|
+
reason: first.reason
|
|
1300
|
+
});
|
|
1301
|
+
try {
|
|
1302
|
+
refreshed = await this.refreshCredentials(signal);
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
if (signal.aborted) throw aborted$2();
|
|
1305
|
+
this.warnDegraded("credential refresh failed", error);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
const second = await this.attempt(image, refreshed, signal);
|
|
1309
|
+
if (second.url !== void 0) return second.url;
|
|
1310
|
+
this.warnDegraded(second.reason);
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
warnDegraded(reason, cause) {
|
|
1314
|
+
this.logger?.warn?.("[image-upload] upload failed, keeping base64 image", {
|
|
1315
|
+
region: this.region,
|
|
1316
|
+
reason
|
|
1317
|
+
}, ...cause === void 0 ? [] : [redactLogValue(cause)]);
|
|
1318
|
+
}
|
|
1319
|
+
async attempt(image, credentials, signal) {
|
|
1320
|
+
const requestId = crypto.randomUUID();
|
|
1321
|
+
const url = getQoderImageUploadUrl(this.region, requestId);
|
|
1322
|
+
const multipart = buildQoderImageMultipart(image.data, image.mediaType);
|
|
1323
|
+
const signedBody = Buffer.from(String(multipart.body.length), "utf8");
|
|
1324
|
+
const timeout = AbortSignal.timeout(this.timeoutMs);
|
|
1325
|
+
const requestSignal = AbortSignal.any([signal, timeout]);
|
|
1326
|
+
const startedAt = this.now();
|
|
1327
|
+
const details = {
|
|
1328
|
+
requestId,
|
|
1329
|
+
region: this.region,
|
|
1330
|
+
mediaType: image.mediaType,
|
|
1331
|
+
bytes: image.data.byteLength,
|
|
1332
|
+
timeoutMs: this.timeoutMs
|
|
1333
|
+
};
|
|
1334
|
+
this.logger?.debug?.("[image-upload] started", details);
|
|
1335
|
+
try {
|
|
1336
|
+
const response = await this.fetchImpl(url, {
|
|
1337
|
+
method: "PUT",
|
|
1338
|
+
headers: {
|
|
1339
|
+
...buildAuthHeaders(signedBody, url, credentials),
|
|
1340
|
+
"AI-CLIENT-TIMESTAMP": String(Math.floor(this.now() / 1e3)),
|
|
1341
|
+
"Content-Type": `multipart/form-data; boundary=${multipart.boundary}`,
|
|
1342
|
+
"Content-Length": String(multipart.body.length),
|
|
1343
|
+
"accept": "application/json"
|
|
1344
|
+
},
|
|
1345
|
+
body: new Uint8Array(multipart.body),
|
|
1346
|
+
signal: requestSignal
|
|
1347
|
+
});
|
|
1348
|
+
this.logger?.debug?.("[image-upload] response received", {
|
|
1349
|
+
requestId,
|
|
1350
|
+
status: response.status,
|
|
1351
|
+
elapsedMs: this.now() - startedAt
|
|
1352
|
+
});
|
|
1353
|
+
const text = await readLimitedText(response, defaultMaxJsonBytes, "Qoder image upload response");
|
|
1354
|
+
if (!response.ok) return {
|
|
1355
|
+
retryable: response.status === 401 || response.status === 403,
|
|
1356
|
+
reason: `HTTP ${response.status}`
|
|
1357
|
+
};
|
|
1358
|
+
let payload;
|
|
1359
|
+
try {
|
|
1360
|
+
payload = JSON.parse(text);
|
|
1361
|
+
} catch {
|
|
1362
|
+
return {
|
|
1363
|
+
retryable: false,
|
|
1364
|
+
reason: "invalid JSON response"
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
const resolved = readQoderImageUrl(payload);
|
|
1368
|
+
if (resolved === void 0) return {
|
|
1369
|
+
retryable: false,
|
|
1370
|
+
reason: "response carried no image URL"
|
|
1371
|
+
};
|
|
1372
|
+
this.logger?.debug?.("[image-upload] succeeded", {
|
|
1373
|
+
...details,
|
|
1374
|
+
status: response.status,
|
|
1375
|
+
elapsedMs: this.now() - startedAt,
|
|
1376
|
+
url: resolved
|
|
1377
|
+
});
|
|
1378
|
+
return {
|
|
1379
|
+
url: resolved,
|
|
1380
|
+
retryable: false,
|
|
1381
|
+
reason: ""
|
|
1382
|
+
};
|
|
1383
|
+
} catch (error) {
|
|
1384
|
+
const failureDetails = {
|
|
1385
|
+
requestId,
|
|
1386
|
+
region: this.region,
|
|
1387
|
+
elapsedMs: this.now() - startedAt
|
|
1388
|
+
};
|
|
1389
|
+
if (signal.aborted) {
|
|
1390
|
+
this.logger?.debug?.("[image-upload] aborted", failureDetails);
|
|
1391
|
+
throw aborted$2();
|
|
1392
|
+
}
|
|
1393
|
+
if (timeout.aborted) {
|
|
1394
|
+
this.logger?.debug?.("[image-upload] timed out", failureDetails);
|
|
1395
|
+
return {
|
|
1396
|
+
retryable: false,
|
|
1397
|
+
reason: "upload timed out"
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
this.logger?.debug?.("[image-upload] network failure", failureDetails, redactLogValue(error));
|
|
1401
|
+
return {
|
|
1402
|
+
retryable: false,
|
|
1403
|
+
reason: "network failure"
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1408
|
+
//#endregion
|
|
1409
|
+
//#region src/qoder/transport/account-reader.ts
|
|
1410
|
+
const defaultUsageTtlMs = 6e4;
|
|
1411
|
+
const defaultUsageTimeoutMs = 15e3;
|
|
1412
|
+
function normalizeQuota(raw) {
|
|
1413
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
1414
|
+
const total = typeof raw.total === "number" && Number.isFinite(raw.total) ? raw.total : typeof raw.cap === "number" && Number.isFinite(raw.cap) ? raw.cap : typeof raw.remaining === "number" && typeof raw.used === "number" ? raw.used + raw.remaining : 0;
|
|
1415
|
+
const used = typeof raw.used === "number" && Number.isFinite(raw.used) ? raw.used : 0;
|
|
1416
|
+
const remaining = typeof raw.remaining === "number" && Number.isFinite(raw.remaining) ? raw.remaining : Math.max(0, total - used);
|
|
1417
|
+
let percentage;
|
|
1418
|
+
if (typeof raw.percentage === "number" && Number.isFinite(raw.percentage)) percentage = raw.percentage <= 1 && total > 1 ? raw.percentage * 100 : raw.percentage;
|
|
1419
|
+
else percentage = total > 0 ? used / total * 100 : 0;
|
|
1420
|
+
const unit = typeof raw.unit === "string" && raw.unit.length > 0 ? raw.unit : "credits";
|
|
1421
|
+
return {
|
|
1422
|
+
total,
|
|
1423
|
+
used,
|
|
1424
|
+
remaining,
|
|
1425
|
+
percentage,
|
|
1426
|
+
unit
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
function normalizeExpiresAt(rawExpires) {
|
|
1430
|
+
if (rawExpires === void 0 || rawExpires === null) return void 0;
|
|
1431
|
+
if (typeof rawExpires === "number" && rawExpires > 0) return new Date(rawExpires).toISOString();
|
|
1432
|
+
if (typeof rawExpires === "string" && rawExpires.length > 0) {
|
|
1433
|
+
const parsed = Date.parse(rawExpires);
|
|
1434
|
+
if (!Number.isNaN(parsed) && parsed > 0) return new Date(parsed).toISOString();
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
function asString(value) {
|
|
1438
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
1439
|
+
}
|
|
1440
|
+
function asBoolean(value) {
|
|
1441
|
+
return typeof value === "boolean" ? value : void 0;
|
|
1442
|
+
}
|
|
1443
|
+
function asNumber(value) {
|
|
1444
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1445
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
1446
|
+
const num = Number(value);
|
|
1447
|
+
if (Number.isFinite(num)) return num;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
function normalizeOrganization(raw) {
|
|
1451
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
1452
|
+
const obj = raw;
|
|
1453
|
+
const orgId = asString(obj.org_id) ?? asString(obj.orgId) ?? asString(obj.id);
|
|
1454
|
+
const orgName = asString(obj.org_name) ?? asString(obj.orgName) ?? asString(obj.name);
|
|
1455
|
+
if (!orgId || !orgName) return void 0;
|
|
1456
|
+
return {
|
|
1457
|
+
orgId,
|
|
1458
|
+
orgName,
|
|
1459
|
+
...asString(obj.role_name) ?? asString(obj.roleName) !== void 0 ? { roleName: asString(obj.role_name) ?? asString(obj.roleName) } : {},
|
|
1460
|
+
isSuspended: asBoolean(obj.is_suspended) ?? asBoolean(obj.isSuspended) ?? false,
|
|
1461
|
+
canManageSubscriptions: asBoolean(obj.can_manage_subscriptions) ?? asBoolean(obj.canManageSubscriptions) ?? false,
|
|
1462
|
+
resourcePackageFeatureEnabled: asBoolean(obj.resource_package_feature_enabled) ?? asBoolean(obj.resourcePackageFeatureEnabled) ?? false
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
function normalizeFeatureAllowed(raw) {
|
|
1466
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
1467
|
+
const obj = raw;
|
|
1468
|
+
return {
|
|
1469
|
+
quest: asBoolean(obj.quest) ?? false,
|
|
1470
|
+
wiki: asBoolean(obj.wiki) ?? false,
|
|
1471
|
+
codeReview: asBoolean(obj.code_review) ?? asBoolean(obj.codeReview) ?? false
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
function normalizePlan(raw) {
|
|
1475
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
1476
|
+
const obj = raw;
|
|
1477
|
+
const userType = asString(obj.user_type) ?? asString(obj.userType);
|
|
1478
|
+
const planTierName = asString(obj.plan_tier_name) ?? asString(obj.planTierName) ?? asString(obj.plan_name) ?? asString(obj.planName);
|
|
1479
|
+
if (!userType || !planTierName) return void 0;
|
|
1480
|
+
const organization = normalizeOrganization(obj.organization);
|
|
1481
|
+
const isPersonalVersion = asBoolean(obj.is_personal_version) ?? asBoolean(obj.isPersonalVersion) ?? organization === void 0;
|
|
1482
|
+
const startDate = normalizeExpiresAt(obj.start_date ?? obj.startDate);
|
|
1483
|
+
const endDate = normalizeExpiresAt(obj.end_date ?? obj.endDate);
|
|
1484
|
+
const planTier = asString(obj.plan_tier) ?? asString(obj.planTier);
|
|
1485
|
+
const isHighestTier = asBoolean(obj.is_highest_tier) ?? asBoolean(obj.isHighestTier);
|
|
1486
|
+
const isRenewed = asBoolean(obj.is_renewed) ?? asBoolean(obj.isRenewed);
|
|
1487
|
+
const featureAllowed = normalizeFeatureAllowed(obj.feature_allowed ?? obj.featureAllowed);
|
|
1488
|
+
return {
|
|
1489
|
+
userType,
|
|
1490
|
+
planTierName,
|
|
1491
|
+
...planTier !== void 0 ? { planTier } : {},
|
|
1492
|
+
isPersonalVersion,
|
|
1493
|
+
...isHighestTier !== void 0 ? { isHighestTier } : {},
|
|
1494
|
+
...isRenewed !== void 0 ? { isRenewed } : {},
|
|
1495
|
+
...startDate !== void 0 ? { startDate } : {},
|
|
1496
|
+
...endDate !== void 0 ? { endDate } : {},
|
|
1497
|
+
...organization !== void 0 ? { organization } : {},
|
|
1498
|
+
...featureAllowed !== void 0 ? { featureAllowed } : {},
|
|
1499
|
+
raw
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
function normalizeStatus(raw) {
|
|
1503
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
1504
|
+
const obj = raw;
|
|
1505
|
+
const featureSwitches = obj.featureSwitches ?? obj.feature_switches;
|
|
1506
|
+
const teamSwitches = obj.teamSwitches ?? obj.team_switches;
|
|
1507
|
+
const allowByok = asNumber(featureSwitches?.allow_byok ?? featureSwitches?.allowByok) ?? 0;
|
|
1508
|
+
const teamAllowByok = asNumber(teamSwitches?.allow_byok ?? teamSwitches?.allowByok);
|
|
1509
|
+
const isPrivacyPolicyModifiable = asBoolean(obj.isPrivacyPolicyModifiable ?? obj.is_data_policy_modifiable);
|
|
1510
|
+
return {
|
|
1511
|
+
allowByok,
|
|
1512
|
+
...teamAllowByok !== void 0 ? { teamAllowByok } : {},
|
|
1513
|
+
...isPrivacyPolicyModifiable !== void 0 ? { isPrivacyPolicyModifiable } : {},
|
|
1514
|
+
raw
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
var QoderUsageReader = class {
|
|
1518
|
+
authService;
|
|
1519
|
+
fetchImpl;
|
|
1520
|
+
ttlMs;
|
|
1521
|
+
timeoutMs;
|
|
1522
|
+
region;
|
|
1523
|
+
logger;
|
|
1524
|
+
cache = /* @__PURE__ */ new Map();
|
|
1525
|
+
flights = new SingleFlight();
|
|
1526
|
+
constructor(options) {
|
|
1527
|
+
this.authService = options.authService;
|
|
1528
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1529
|
+
this.ttlMs = options.ttlMs ?? defaultUsageTtlMs;
|
|
1530
|
+
this.timeoutMs = options.timeoutMs ?? defaultUsageTimeoutMs;
|
|
1531
|
+
this.region = options.region ?? "global";
|
|
1532
|
+
this.logger = options.logger;
|
|
1533
|
+
}
|
|
1534
|
+
async readAccount(pat, options) {
|
|
1535
|
+
if (!pat || typeof pat !== "string") throw new QoderLlmError("Qoder Personal Access Token is missing or invalid.", "MISSING_CREDENTIAL");
|
|
1536
|
+
const cacheKey = `${this.region}:${opaqueCredentialKey(pat)}`;
|
|
1537
|
+
if (!options?.force) {
|
|
1538
|
+
const cached = this.cache.get(cacheKey);
|
|
1539
|
+
if (cached && cached.expiresAt > Date.now()) return cached.info;
|
|
1540
|
+
}
|
|
1541
|
+
return this.flights.run(cacheKey, options?.signal, (sharedSignal) => this.loadAccount(pat, sharedSignal, cacheKey), () => new QoderLlmError("Qoder account request was aborted.", "ABORTED"));
|
|
1542
|
+
}
|
|
1543
|
+
async loadAccount(pat, signal, cacheKey) {
|
|
1544
|
+
const creds = await this.authService.getCredentials(pat, signal);
|
|
1545
|
+
const profile = {
|
|
1546
|
+
id: creds.userID,
|
|
1547
|
+
name: creds.name || "Qoder User",
|
|
1548
|
+
email: creds.email || ""
|
|
1549
|
+
};
|
|
1550
|
+
const [usage, plan, status] = await Promise.all([
|
|
1551
|
+
retryMetadataRead(signal, () => this.fetchUsage(creds.authToken, signal)),
|
|
1552
|
+
this.safeFetchPlan(creds.authToken, signal),
|
|
1553
|
+
this.safeFetchStatus(creds.authToken, creds.machineID, signal)
|
|
1554
|
+
]);
|
|
1555
|
+
const accountInfo = {
|
|
1556
|
+
profile,
|
|
1557
|
+
usage,
|
|
1558
|
+
...plan !== void 0 ? { plan } : {},
|
|
1559
|
+
...status !== void 0 ? { status } : {},
|
|
1560
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1561
|
+
};
|
|
1562
|
+
this.cache.set(cacheKey, {
|
|
1563
|
+
info: accountInfo,
|
|
1564
|
+
expiresAt: Date.now() + this.ttlMs
|
|
1565
|
+
});
|
|
1566
|
+
return accountInfo;
|
|
1567
|
+
}
|
|
1568
|
+
clear(pat) {
|
|
1569
|
+
if (pat) this.cache.delete(`${this.region}:${opaqueCredentialKey(pat)}`);
|
|
1570
|
+
else this.cache.clear();
|
|
1571
|
+
}
|
|
1572
|
+
async fetchUsage(jobToken, signal) {
|
|
1573
|
+
const data = await openApiJsonRequest(this.fetchImpl, {
|
|
1574
|
+
url: getQoderUsageUrl(this.region),
|
|
1575
|
+
token: jobToken,
|
|
1576
|
+
signal,
|
|
1577
|
+
timeoutMs: this.timeoutMs,
|
|
1578
|
+
logger: this.logger,
|
|
1579
|
+
operation: "Usage",
|
|
1580
|
+
logCategory: "account.usage"
|
|
1581
|
+
});
|
|
1582
|
+
return {
|
|
1583
|
+
userQuota: normalizeQuota(data.userQuota),
|
|
1584
|
+
orgResourcePackage: normalizeQuota(data.orgResourcePackage),
|
|
1585
|
+
totalUsagePercentage: typeof data.totalUsagePercentage === "number" ? data.totalUsagePercentage : void 0,
|
|
1586
|
+
isQuotaExceeded: typeof data.isQuotaExceeded === "boolean" ? data.isQuotaExceeded : false,
|
|
1587
|
+
expiresAt: normalizeExpiresAt(data.expiresAt),
|
|
1588
|
+
raw: data
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
async fetchPlan(jobToken, signal) {
|
|
1592
|
+
return normalizePlan(await openApiJsonRequest(this.fetchImpl, {
|
|
1593
|
+
url: getQoderUserPlanUrl(this.region),
|
|
1594
|
+
token: jobToken,
|
|
1595
|
+
signal,
|
|
1596
|
+
timeoutMs: this.timeoutMs,
|
|
1597
|
+
logger: this.logger,
|
|
1598
|
+
operation: "Plan",
|
|
1599
|
+
logCategory: "account.plan"
|
|
1600
|
+
}));
|
|
1601
|
+
}
|
|
1602
|
+
async safeFetchPlan(jobToken, signal) {
|
|
1603
|
+
try {
|
|
1604
|
+
return await this.fetchPlan(jobToken, signal);
|
|
1605
|
+
} catch (error) {
|
|
1606
|
+
if (signal.aborted) throw error;
|
|
1607
|
+
this.logger?.warn?.("[Qoder Plan] Failed to load user plan (degraded)", error instanceof Error ? error.message : error);
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
async fetchStatus(jobToken, machineId, signal) {
|
|
1612
|
+
return normalizeStatus(await openApiJsonRequest(this.fetchImpl, {
|
|
1613
|
+
url: getQoderUserStatusUrl(this.region),
|
|
1614
|
+
token: jobToken,
|
|
1615
|
+
machineId,
|
|
1616
|
+
signal,
|
|
1617
|
+
timeoutMs: this.timeoutMs,
|
|
1618
|
+
logger: this.logger,
|
|
1619
|
+
operation: "Status",
|
|
1620
|
+
logCategory: "account.status"
|
|
1621
|
+
}));
|
|
1622
|
+
}
|
|
1623
|
+
async safeFetchStatus(jobToken, machineId, signal) {
|
|
1624
|
+
try {
|
|
1625
|
+
return await this.fetchStatus(jobToken, machineId, signal);
|
|
1626
|
+
} catch (error) {
|
|
1627
|
+
if (signal?.aborted) throw error;
|
|
1628
|
+
this.logger?.warn?.("[Qoder Status] Failed to load user status (degraded)", error instanceof Error ? error.message : error);
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
};
|
|
1633
|
+
//#endregion
|
|
1634
|
+
//#region src/qoder/transport/wire/encoding.ts
|
|
1635
|
+
/**
|
|
1636
|
+
* Qoder WAF Body encoding algorithm.
|
|
1637
|
+
*
|
|
1638
|
+
* Re-arranges standard Base64 chunks and translates characters against
|
|
1639
|
+
* a custom alphabet to satisfy Qoder upstream WAF requirements.
|
|
1640
|
+
*
|
|
1641
|
+
* @module dsh-provider-qoder/qoder/transport/wire/encoding
|
|
1642
|
+
*/
|
|
1643
|
+
const qoderCustomAlphabet = "_doRTgHZBKcGVjlvpC,@aFSx#DPuNJme&i*MzLOEn)sUrthbf%Y^w.(kIQyXqWA!";
|
|
1644
|
+
const qoderStdAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
1645
|
+
function qoderEncodeBody(plaintext) {
|
|
1646
|
+
const std = Buffer.isBuffer(plaintext) ? plaintext.toString("base64") : Buffer.from(plaintext).toString("base64");
|
|
1647
|
+
const n = std.length;
|
|
1648
|
+
const a = Math.floor(n / 3);
|
|
1649
|
+
const rearranged = std.slice(n - a) + std.slice(a, n - a) + std.slice(0, a);
|
|
1650
|
+
let out = "";
|
|
1651
|
+
for (let i = 0; i < n; i++) {
|
|
1652
|
+
const c = rearranged[i];
|
|
1653
|
+
if (c === "=") out += "$";
|
|
1654
|
+
else {
|
|
1655
|
+
const idx = qoderStdAlphabet.indexOf(c);
|
|
1656
|
+
if (idx >= 0) out += qoderCustomAlphabet[idx];
|
|
1657
|
+
else out += c;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
return out;
|
|
644
1661
|
}
|
|
645
1662
|
//#endregion
|
|
646
|
-
//#region src/thinking.ts
|
|
1663
|
+
//#region src/qoder/transport/wire/thinking.ts
|
|
647
1664
|
const thinkingTags = [{
|
|
648
1665
|
open: "<thinking>",
|
|
649
1666
|
close: "</thinking>"
|
|
@@ -800,7 +1817,7 @@ var QoderThinkingParser = class {
|
|
|
800
1817
|
}
|
|
801
1818
|
};
|
|
802
1819
|
//#endregion
|
|
803
|
-
//#region src/sse.ts
|
|
1820
|
+
//#region src/qoder/transport/wire/sse.ts
|
|
804
1821
|
/** Decode Qoder's outer SSE envelope and inner model chunks. */
|
|
805
1822
|
const doneMarker = "[DONE]";
|
|
806
1823
|
function malformed(message) {
|
|
@@ -866,6 +1883,7 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
866
1883
|
let pendingUsage;
|
|
867
1884
|
let terminalKind = "stop";
|
|
868
1885
|
let sawContent = false;
|
|
1886
|
+
const maxBufferChars = options.maxBufferChars ?? 2097152;
|
|
869
1887
|
const closeTextual = () => {
|
|
870
1888
|
if (activeTextual === void 0) return [];
|
|
871
1889
|
const state = activeTextual;
|
|
@@ -942,6 +1960,7 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
942
1960
|
} else {
|
|
943
1961
|
options.onActivity?.();
|
|
944
1962
|
buffer += decoder.decode(value, { stream: true });
|
|
1963
|
+
if (buffer.length > maxBufferChars) throw malformed("Qoder SSE frame exceeded its size limit.");
|
|
945
1964
|
}
|
|
946
1965
|
while (!sawDone) {
|
|
947
1966
|
const lineEnd = buffer.indexOf("\n");
|
|
@@ -987,16 +2006,22 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
987
2006
|
};
|
|
988
2007
|
toolCalls.set(upstreamIndex, state);
|
|
989
2008
|
}
|
|
990
|
-
if (rawCall.id !== void 0) {
|
|
991
|
-
|
|
2009
|
+
if (rawCall.id !== void 0) if (rawCall.id === "" || rawCall.id === null) {} else if (typeof rawCall.id !== "string") throw malformed("Qoder tool call has an invalid id.");
|
|
2010
|
+
else {
|
|
992
2011
|
if (state.id && state.id !== rawCall.id) throw malformed("Qoder changed a streamed tool-call id.");
|
|
993
2012
|
state.id = rawCall.id;
|
|
994
2013
|
}
|
|
2014
|
+
let hasNewName = false;
|
|
995
2015
|
if (rawCall.function?.name !== void 0) {
|
|
996
2016
|
const name = rawCall.function.name;
|
|
997
|
-
if (
|
|
998
|
-
|
|
999
|
-
|
|
2017
|
+
if (name === "" || name === null) {} else if (typeof name !== "string") throw malformed("Qoder tool call has an invalid name.");
|
|
2018
|
+
else {
|
|
2019
|
+
if (state.name && state.name !== name) throw malformed("Qoder changed a streamed tool-call name.");
|
|
2020
|
+
if (state.name !== name) {
|
|
2021
|
+
state.name = name;
|
|
2022
|
+
hasNewName = true;
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
1000
2025
|
}
|
|
1001
2026
|
if (rawCall.function?.arguments !== void 0) {
|
|
1002
2027
|
if (typeof rawCall.function.arguments !== "string") throw malformed("Qoder tool-call arguments delta is not a string.");
|
|
@@ -1014,7 +2039,7 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
1014
2039
|
...state.name ? { name: state.name } : {},
|
|
1015
2040
|
argumentsDelta
|
|
1016
2041
|
};
|
|
1017
|
-
} else if (wasOpen && state.blockIndex !== void 0 &&
|
|
2042
|
+
} else if (wasOpen && state.blockIndex !== void 0 && hasNewName) yield {
|
|
1018
2043
|
type: "tool-call-delta",
|
|
1019
2044
|
index: state.blockIndex,
|
|
1020
2045
|
id: ToolCallId(state.id),
|
|
@@ -1071,531 +2096,220 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
1071
2096
|
reason: {
|
|
1072
2097
|
kind: "error",
|
|
1073
2098
|
failure: {
|
|
1074
|
-
message: "Model returned a completed response with no content.",
|
|
1075
|
-
code: EMPTY_RESPONSE_CODE
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
};
|
|
1079
|
-
return;
|
|
1080
|
-
}
|
|
1081
|
-
yield {
|
|
1082
|
-
type: "finish",
|
|
1083
|
-
reason: { kind: toolCalls.size > 0 ? "tool-calls" : terminalKind }
|
|
1084
|
-
};
|
|
1085
|
-
}
|
|
1086
|
-
//#endregion
|
|
1087
|
-
//#region src/adapter.ts
|
|
1088
|
-
/** DSH LlmAdapter for the Global Qoder subscription transport. */
|
|
1089
|
-
const defaultMaxTokens = 32768;
|
|
1090
|
-
const defaultStreamIdleTimeoutMs = 300 * 1e3;
|
|
1091
|
-
const defaultModels = [
|
|
1092
|
-
{
|
|
1093
|
-
id: "cmodel",
|
|
1094
|
-
name: "Cantus (Qoder)",
|
|
1095
|
-
description: "Default Global Qoder subscription model for quick validation",
|
|
1096
|
-
contextWindow: 1e6,
|
|
1097
|
-
maxTokens: defaultMaxTokens
|
|
1098
|
-
},
|
|
1099
|
-
{
|
|
1100
|
-
id: "auto",
|
|
1101
|
-
name: "Qoder Auto",
|
|
1102
|
-
description: "Server-routed Global Qoder model pool",
|
|
1103
|
-
contextWindow: 18e4,
|
|
1104
|
-
maxTokens: defaultMaxTokens
|
|
1105
|
-
},
|
|
1106
|
-
{
|
|
1107
|
-
id: "ultimate",
|
|
1108
|
-
name: "Qoder Ultimate",
|
|
1109
|
-
description: "Highest-capability Global Qoder model pool",
|
|
1110
|
-
contextWindow: 1e6,
|
|
1111
|
-
maxTokens: defaultMaxTokens
|
|
1112
|
-
},
|
|
1113
|
-
{
|
|
1114
|
-
id: "performance",
|
|
1115
|
-
name: "Qoder Performance",
|
|
1116
|
-
description: "Performance-oriented Global Qoder model pool",
|
|
1117
|
-
contextWindow: 1e6,
|
|
1118
|
-
maxTokens: defaultMaxTokens
|
|
1119
|
-
},
|
|
1120
|
-
{
|
|
1121
|
-
id: "efficient",
|
|
1122
|
-
name: "Qoder Efficient",
|
|
1123
|
-
description: "Efficiency-oriented Global Qoder model pool",
|
|
1124
|
-
contextWindow: 18e4,
|
|
1125
|
-
maxTokens: defaultMaxTokens
|
|
1126
|
-
},
|
|
1127
|
-
{
|
|
1128
|
-
id: "lite",
|
|
1129
|
-
name: "Qoder Lite",
|
|
1130
|
-
description: "Basic Global Qoder model pool",
|
|
1131
|
-
contextWindow: 18e4,
|
|
1132
|
-
maxTokens: defaultMaxTokens
|
|
1133
|
-
}
|
|
1134
|
-
];
|
|
1135
|
-
function modelInfo(provider, model) {
|
|
1136
|
-
return {
|
|
1137
|
-
provider,
|
|
1138
|
-
id: model.id,
|
|
1139
|
-
name: model.priceFactor === void 0 ? model.name : `${model.name} (${model.priceFactor}x)`,
|
|
1140
|
-
description: model.description,
|
|
1141
|
-
inputModalities: ["text"]
|
|
1142
|
-
};
|
|
1143
|
-
}
|
|
1144
|
-
var QoderAdapter = class extends LlmAdapter {
|
|
1145
|
-
resolvePat;
|
|
1146
|
-
catalogModels;
|
|
1147
|
-
authService;
|
|
1148
|
-
fetchImpl;
|
|
1149
|
-
logger;
|
|
1150
|
-
streamIdleTimeoutMs;
|
|
1151
|
-
providerId;
|
|
1152
|
-
providerName;
|
|
1153
|
-
region;
|
|
1154
|
-
constructor(options) {
|
|
1155
|
-
super();
|
|
1156
|
-
this.resolvePat = options.resolvePat;
|
|
1157
|
-
this.catalogModels = options.models && options.models.length > 0 ? options.models : defaultModels;
|
|
1158
|
-
this.authService = options.authService ?? new QoderAuthService({ fetch: options.fetch });
|
|
1159
|
-
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1160
|
-
this.logger = options.logger;
|
|
1161
|
-
this.streamIdleTimeoutMs = options.streamIdleTimeoutMs ?? 3e5;
|
|
1162
|
-
this.providerId = options.providerId ?? "qoder-official";
|
|
1163
|
-
this.providerName = options.providerName ?? "Qoder";
|
|
1164
|
-
this.region = options.region ?? "global";
|
|
1165
|
-
}
|
|
1166
|
-
providerInfo(provider) {
|
|
1167
|
-
return {
|
|
1168
|
-
id: provider,
|
|
1169
|
-
name: this.providerName
|
|
1170
|
-
};
|
|
1171
|
-
}
|
|
1172
|
-
listModels(provider) {
|
|
1173
|
-
return Promise.resolve(this.catalogModels.map((model) => modelInfo(provider, model)));
|
|
1174
|
-
}
|
|
1175
|
-
replaceConfig(models, streamIdleTimeoutMs, region = "global") {
|
|
1176
|
-
this.catalogModels = models;
|
|
1177
|
-
this.streamIdleTimeoutMs = streamIdleTimeoutMs;
|
|
1178
|
-
this.region = region;
|
|
1179
|
-
}
|
|
1180
|
-
resolveModel(provider, modelId, signal) {
|
|
1181
|
-
if (signal?.aborted) return Promise.reject(new QoderLlmError("Qoder model resolution was aborted.", "ABORTED"));
|
|
1182
|
-
const configured = this.catalogModels.find((model) => model.id === modelId);
|
|
1183
|
-
if (configured === void 0) return Promise.resolve({
|
|
1184
|
-
provider,
|
|
1185
|
-
id: modelId,
|
|
1186
|
-
name: modelId,
|
|
1187
|
-
inputModalities: ["text"]
|
|
1188
|
-
});
|
|
1189
|
-
return Promise.resolve({
|
|
1190
|
-
...modelInfo(provider, configured),
|
|
1191
|
-
...configured.contextWindow === void 0 ? {} : { context: { contextWindow: configured.contextWindow } },
|
|
1192
|
-
...configured.maxTokens === void 0 ? {} : { defaultMaxTokens: configured.maxTokens },
|
|
1193
|
-
...configured.reasoningEfforts === void 0 || configured.reasoningEfforts.length === 0 ? {} : { reasoning: {
|
|
1194
|
-
efforts: configured.reasoningEfforts.map((effort) => ({
|
|
1195
|
-
id: ReasoningEffortId(effort.id),
|
|
1196
|
-
name: effort.name,
|
|
1197
|
-
...effort.description === void 0 ? {} : { description: effort.description }
|
|
1198
|
-
})),
|
|
1199
|
-
...configured.defaultReasoningEffort === void 0 ? {} : { defaultEffort: ReasoningEffortId(configured.defaultReasoningEffort) }
|
|
1200
|
-
} }
|
|
1201
|
-
});
|
|
1202
|
-
}
|
|
1203
|
-
stream(options) {
|
|
1204
|
-
return this.generate(options);
|
|
1205
|
-
}
|
|
1206
|
-
async *generate(options) {
|
|
1207
|
-
if (options.provider !== this.providerId) throw new QoderLlmError(`Qoder adapter does not own provider "${options.provider}".`, "INVALID_PROVIDER");
|
|
1208
|
-
if (options.signal?.aborted) throw new QoderLlmError("Request was aborted prior to generation.", "ABORTED");
|
|
1209
|
-
const model = this.catalogModels.find((candidate) => candidate.id === (options.model || "cmodel"));
|
|
1210
|
-
const messages = validateQoderRequest(options, model);
|
|
1211
|
-
const pat = await this.resolvePat();
|
|
1212
|
-
if (!pat) throw new QoderLlmError("Qoder Personal Access Token is missing. Configure Qoder in the Qoder settings page.", "MISSING_CREDENTIAL");
|
|
1213
|
-
if (options.signal?.aborted) throw new QoderLlmError("Request was aborted.", "ABORTED");
|
|
1214
|
-
const credentials = await this.authService.getCredentials(pat, options.signal, this.region);
|
|
1215
|
-
const request = buildQoderRequestBody(options, credentials.userID, messages, model);
|
|
1216
|
-
const encodedBody = qoderEncodeBody(JSON.stringify(request));
|
|
1217
|
-
const encodedBytes = Buffer.from(encodedBody, "utf8");
|
|
1218
|
-
const chatUrl = getQoderChatUrl(this.region);
|
|
1219
|
-
const requestController = new AbortController();
|
|
1220
|
-
let idleTimedOut = false;
|
|
1221
|
-
let idleTimer;
|
|
1222
|
-
const onCallerAbort = () => requestController.abort(options.signal?.reason);
|
|
1223
|
-
options.signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
1224
|
-
const resetIdleTimer = () => {
|
|
1225
|
-
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
1226
|
-
idleTimer = setTimeout(() => {
|
|
1227
|
-
idleTimedOut = true;
|
|
1228
|
-
requestController.abort("stream idle timeout");
|
|
1229
|
-
}, this.streamIdleTimeoutMs);
|
|
1230
|
-
};
|
|
1231
|
-
resetIdleTimer();
|
|
1232
|
-
try {
|
|
1233
|
-
const response = await this.fetchImpl(chatUrl, {
|
|
1234
|
-
method: "POST",
|
|
1235
|
-
headers: {
|
|
1236
|
-
"content-type": "application/json",
|
|
1237
|
-
"accept": "text/event-stream",
|
|
1238
|
-
"cache-control": "no-cache",
|
|
1239
|
-
"accept-encoding": "identity",
|
|
1240
|
-
"x-model-key": options.model || "cmodel",
|
|
1241
|
-
"x-model-source": model?.source || "system",
|
|
1242
|
-
...attributionHeaders(),
|
|
1243
|
-
...buildAuthHeaders(encodedBytes, chatUrl, credentials)
|
|
1244
|
-
},
|
|
1245
|
-
body: encodedBytes,
|
|
1246
|
-
signal: requestController.signal
|
|
1247
|
-
});
|
|
1248
|
-
resetIdleTimer();
|
|
1249
|
-
if (!response.ok) throw qoderHttpError(`Qoder upstream service returned HTTP ${response.status}.`, response);
|
|
1250
|
-
if (!response.body) throw new QoderLlmError("Qoder response contains no readable body stream.", "EMPTY_RESPONSE");
|
|
1251
|
-
yield* parseQoderSse(response.body, { onActivity: resetIdleTimer });
|
|
1252
|
-
} catch (error) {
|
|
1253
|
-
if (options.signal?.aborted) throw new QoderLlmError("Request was aborted.", "ABORTED");
|
|
1254
|
-
if (idleTimedOut) {
|
|
1255
|
-
const failure = new QoderLlmError("Qoder model stream exceeded its idle timeout.", "TIMEOUT");
|
|
1256
|
-
this.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(failure));
|
|
1257
|
-
throw failure;
|
|
1258
|
-
}
|
|
1259
|
-
if (error instanceof QoderLlmError) {
|
|
1260
|
-
this.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(error));
|
|
1261
|
-
throw error;
|
|
1262
|
-
}
|
|
1263
|
-
const failure = new QoderLlmError("Qoder transport request failed.", "TRANSPORT", { cause: error });
|
|
1264
|
-
this.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(failure), { cause: redactLogValue(error) });
|
|
1265
|
-
throw failure;
|
|
1266
|
-
} finally {
|
|
1267
|
-
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
1268
|
-
options.signal?.removeEventListener("abort", onCallerAbort);
|
|
1269
|
-
requestController.abort("request complete");
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
};
|
|
1273
|
-
//#endregion
|
|
1274
|
-
//#region src/credential-contract.ts
|
|
1275
|
-
/** Shared Host/browser contract for the one managed Qoder credential. */
|
|
1276
|
-
const qoderCredentialRef = "QODER_MANAGED_CREDENTIAL";
|
|
1277
|
-
/** DSH's local provider uses `env` and `*-env` for ambient credential layers. */
|
|
1278
|
-
function isEnvironmentCredentialSource(source) {
|
|
1279
|
-
return source === "env" || source?.endsWith("-env") === true;
|
|
1280
|
-
}
|
|
1281
|
-
//#endregion
|
|
1282
|
-
//#region src/credential.ts
|
|
1283
|
-
/** Host-side resolution of the one managed Qoder credential. */
|
|
1284
|
-
const managedQoderCredentialRef = credentialRef(qoderCredentialRef);
|
|
1285
|
-
/** Resolve only provider-managed storage; ambient environment layers are intentionally unsupported. */
|
|
1286
|
-
async function resolveManagedQoderPat(credentials) {
|
|
1287
|
-
const hit = await credentials.resolve(managedQoderCredentialRef);
|
|
1288
|
-
if (hit === void 0 || isEnvironmentCredentialSource(hit.source)) return "";
|
|
1289
|
-
return hit.value.trim();
|
|
1290
|
-
}
|
|
1291
|
-
//#endregion
|
|
1292
|
-
//#region src/models.ts
|
|
1293
|
-
const discoveredMetadataKeys = [
|
|
1294
|
-
"description",
|
|
1295
|
-
"source",
|
|
1296
|
-
"isReasoning",
|
|
1297
|
-
"supportsEffort",
|
|
1298
|
-
"reasoningEfforts",
|
|
1299
|
-
"defaultReasoningEffort",
|
|
1300
|
-
"priceFactor",
|
|
1301
|
-
"contextOptions"
|
|
1302
|
-
];
|
|
1303
|
-
function positiveNumber(value) {
|
|
1304
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
1305
|
-
}
|
|
1306
|
-
const reasoningEffortOrder = /* @__PURE__ */ new Map([
|
|
1307
|
-
["low", 0],
|
|
1308
|
-
["medium", 1],
|
|
1309
|
-
["high", 2],
|
|
1310
|
-
["xhigh", 3],
|
|
1311
|
-
["max", 4]
|
|
1312
|
-
]);
|
|
1313
|
-
function contextOptionsOf(value) {
|
|
1314
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
1315
|
-
const options = {};
|
|
1316
|
-
for (const [key, raw] of Object.entries(value)) {
|
|
1317
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
|
|
1318
|
-
const entry = raw;
|
|
1319
|
-
const tokenCount = positiveNumber(entry.token_count);
|
|
1320
|
-
const isDefault = typeof entry.is_default === "boolean" ? entry.is_default : void 0;
|
|
1321
|
-
if (tokenCount === void 0 && isDefault === void 0) continue;
|
|
1322
|
-
options[key] = {
|
|
1323
|
-
...tokenCount === void 0 ? {} : { tokenCount },
|
|
1324
|
-
...isDefault === void 0 ? {} : { isDefault }
|
|
1325
|
-
};
|
|
1326
|
-
}
|
|
1327
|
-
if (Object.keys(options).length === 0) return void 0;
|
|
1328
|
-
const largest = Math.max(...Object.values(options).map((option) => option.tokenCount ?? 0));
|
|
1329
|
-
if (largest <= 0) return options;
|
|
1330
|
-
return Object.fromEntries(Object.entries(options).map(([key, option]) => [key, {
|
|
1331
|
-
...option,
|
|
1332
|
-
isDefault: option.tokenCount === largest
|
|
1333
|
-
}]));
|
|
1334
|
-
}
|
|
1335
|
-
function reasoningEffortsOf(value) {
|
|
1336
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
|
|
1337
|
-
const enabled = value.enabled;
|
|
1338
|
-
if (typeof enabled !== "object" || enabled === null || Array.isArray(enabled)) return {};
|
|
1339
|
-
const rawEfforts = enabled.efforts;
|
|
1340
|
-
if (typeof rawEfforts !== "object" || rawEfforts === null || Array.isArray(rawEfforts)) return {};
|
|
1341
|
-
const efforts = [];
|
|
1342
|
-
let defaultEffort;
|
|
1343
|
-
for (const [id, raw] of Object.entries(rawEfforts)) {
|
|
1344
|
-
if (!id.trim() || typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
|
|
1345
|
-
const entry = raw;
|
|
1346
|
-
efforts.push({
|
|
1347
|
-
id,
|
|
1348
|
-
name: id,
|
|
1349
|
-
...typeof entry.description === "string" && entry.description.trim() ? { description: entry.description.trim() } : {}
|
|
1350
|
-
});
|
|
1351
|
-
if (entry.is_default === true) defaultEffort = id;
|
|
1352
|
-
}
|
|
1353
|
-
return {
|
|
1354
|
-
...efforts.length === 0 ? {} : { efforts: efforts.sort((left, right) => (reasoningEffortOrder.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (reasoningEffortOrder.get(right.id) ?? Number.MAX_SAFE_INTEGER)) },
|
|
1355
|
-
...defaultEffort === void 0 ? {} : { defaultEffort }
|
|
1356
|
-
};
|
|
1357
|
-
}
|
|
1358
|
-
function normalizeQoderModels(payload) {
|
|
1359
|
-
if (typeof payload !== "object" || payload === null || !Array.isArray(payload.chat)) return [];
|
|
1360
|
-
const models = [];
|
|
1361
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1362
|
-
for (const raw of payload.chat) {
|
|
1363
|
-
if (typeof raw !== "object" || raw === null || raw.enable !== true) continue;
|
|
1364
|
-
const id = typeof raw.key === "string" ? raw.key.trim() : "";
|
|
1365
|
-
if (!id || seen.has(id)) continue;
|
|
1366
|
-
seen.add(id);
|
|
1367
|
-
const contextOptions = contextOptionsOf(raw.context_config);
|
|
1368
|
-
const contextWindow = Math.max(positiveNumber(raw.max_input_tokens) ?? 18e4, ...Object.values(contextOptions ?? {}).map((option) => option.tokenCount ?? 0));
|
|
1369
|
-
const thinking = typeof raw.thinking_config === "object" && raw.thinking_config !== null;
|
|
1370
|
-
const reasoning = reasoningEffortsOf(raw.thinking_config);
|
|
1371
|
-
models.push({
|
|
1372
|
-
id,
|
|
1373
|
-
name: typeof raw.display_name === "string" && raw.display_name.trim() ? raw.display_name.trim() : id,
|
|
1374
|
-
contextWindow,
|
|
1375
|
-
maxTokens: positiveNumber(raw.max_output_tokens) ?? 32768,
|
|
1376
|
-
source: typeof raw.source === "string" && raw.source.trim() ? raw.source.trim() : "system",
|
|
1377
|
-
isReasoning: raw.is_reasoning === true || thinking,
|
|
1378
|
-
supportsEffort: reasoning.efforts !== void 0,
|
|
1379
|
-
...reasoning.efforts === void 0 ? {} : { reasoningEfforts: reasoning.efforts },
|
|
1380
|
-
...reasoning.defaultEffort === void 0 ? {} : { defaultReasoningEffort: reasoning.defaultEffort },
|
|
1381
|
-
...positiveNumber(raw.price_factor) === void 0 ? {} : { priceFactor: positiveNumber(raw.price_factor) },
|
|
1382
|
-
...contextOptions === void 0 ? {} : { contextOptions }
|
|
1383
|
-
});
|
|
2099
|
+
message: "Model returned a completed response with no content.",
|
|
2100
|
+
code: EMPTY_RESPONSE_CODE
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
};
|
|
2104
|
+
return;
|
|
1384
2105
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
* has retained only id, name, and token limits.
|
|
1390
|
-
*/
|
|
1391
|
-
function mergeQoderDiscoveryMetadata(configured, discovered) {
|
|
1392
|
-
const catalog = new Map(discovered.map((model) => [model.id, model]));
|
|
1393
|
-
return configured.map((model) => {
|
|
1394
|
-
const advertised = catalog.get(model.id);
|
|
1395
|
-
if (advertised === void 0) return { ...model };
|
|
1396
|
-
const merged = { ...model };
|
|
1397
|
-
for (const key of discoveredMetadataKeys) delete merged[key];
|
|
1398
|
-
for (const key of discoveredMetadataKeys) if (advertised[key] !== void 0) Object.assign(merged, { [key]: advertised[key] });
|
|
1399
|
-
return merged;
|
|
1400
|
-
});
|
|
2106
|
+
yield {
|
|
2107
|
+
type: "finish",
|
|
2108
|
+
reason: { kind: toolCalls.size > 0 ? "tool-calls" : terminalKind }
|
|
2109
|
+
};
|
|
1401
2110
|
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
2111
|
+
//#endregion
|
|
2112
|
+
//#region src/qoder/transport/chat.ts
|
|
2113
|
+
/** Qoder chat request encoding, timeout lifecycle, and SSE streaming. */
|
|
2114
|
+
function aborted$1(message) {
|
|
2115
|
+
return new QoderLlmError(message, "ABORTED");
|
|
1407
2116
|
}
|
|
1408
|
-
async function
|
|
1409
|
-
const
|
|
1410
|
-
const
|
|
1411
|
-
|
|
1412
|
-
|
|
2117
|
+
async function* streamQoderChat(options, model, credentials, messages, dependencies) {
|
|
2118
|
+
const request = await buildQoderRequestBody(options, credentials.userID, messages, model);
|
|
2119
|
+
const encodedBody = qoderEncodeBody(JSON.stringify(request));
|
|
2120
|
+
const encodedBytes = Buffer.from(encodedBody, "utf8");
|
|
2121
|
+
const chatUrl = getQoderChatUrl(dependencies.region);
|
|
2122
|
+
const requestController = new AbortController();
|
|
2123
|
+
let headerTimedOut = false;
|
|
2124
|
+
let idleTimedOut = false;
|
|
2125
|
+
let idleTimer;
|
|
2126
|
+
const startedAt = performance.now();
|
|
2127
|
+
const onCallerAbort = () => requestController.abort(options.signal?.reason);
|
|
2128
|
+
options.signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
2129
|
+
const headerTimer = setTimeout(() => {
|
|
2130
|
+
headerTimedOut = true;
|
|
2131
|
+
requestController.abort("response header timeout");
|
|
2132
|
+
}, dependencies.responseHeaderTimeoutMs);
|
|
2133
|
+
const resetIdleTimer = () => {
|
|
2134
|
+
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
2135
|
+
idleTimer = setTimeout(() => {
|
|
2136
|
+
idleTimedOut = true;
|
|
2137
|
+
requestController.abort("stream idle timeout");
|
|
2138
|
+
}, dependencies.streamIdleTimeoutMs);
|
|
2139
|
+
};
|
|
1413
2140
|
try {
|
|
1414
|
-
response = await
|
|
1415
|
-
method: "
|
|
2141
|
+
const response = await dependencies.fetch(chatUrl, {
|
|
2142
|
+
method: "POST",
|
|
1416
2143
|
headers: {
|
|
1417
|
-
|
|
1418
|
-
|
|
2144
|
+
"content-type": "application/json",
|
|
2145
|
+
"accept": "text/event-stream",
|
|
2146
|
+
"cache-control": "no-cache",
|
|
2147
|
+
"accept-encoding": "identity",
|
|
2148
|
+
"x-model-key": options.model || "cmodel",
|
|
2149
|
+
"x-model-source": model?.source || "system",
|
|
2150
|
+
...attributionHeaders(),
|
|
2151
|
+
...buildAuthHeaders(encodedBytes, chatUrl, credentials)
|
|
1419
2152
|
},
|
|
1420
|
-
|
|
2153
|
+
body: encodedBytes,
|
|
2154
|
+
signal: requestController.signal
|
|
2155
|
+
});
|
|
2156
|
+
clearTimeout(headerTimer);
|
|
2157
|
+
dependencies.logger?.debug?.("[Qoder Stream] Response headers received", {
|
|
2158
|
+
region: dependencies.region,
|
|
2159
|
+
status: response.status,
|
|
2160
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
2161
|
+
...qoderRequestId(response.headers) === void 0 ? {} : { requestId: qoderRequestId(response.headers) }
|
|
1421
2162
|
});
|
|
2163
|
+
resetIdleTimer();
|
|
2164
|
+
if (!response.ok) throw qoderHttpError(`Qoder upstream service returned HTTP ${response.status}.`, response);
|
|
2165
|
+
if (!response.body) throw new QoderLlmError("Qoder response contains no readable body stream.", "EMPTY_RESPONSE");
|
|
2166
|
+
yield* parseQoderSse(response.body, { onActivity: resetIdleTimer });
|
|
1422
2167
|
} catch (error) {
|
|
1423
|
-
if (options.signal?.aborted) throw
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
throw
|
|
2168
|
+
if (options.signal?.aborted) throw aborted$1("Request was aborted.");
|
|
2169
|
+
if (headerTimedOut) {
|
|
2170
|
+
const failure = new QoderLlmError("Qoder model request exceeded its response header timeout.", "TIMEOUT");
|
|
2171
|
+
dependencies.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(failure));
|
|
2172
|
+
throw failure;
|
|
2173
|
+
}
|
|
2174
|
+
if (idleTimedOut) {
|
|
2175
|
+
const failure = new QoderLlmError("Qoder model stream exceeded its idle timeout.", "TIMEOUT");
|
|
2176
|
+
dependencies.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(failure));
|
|
2177
|
+
throw failure;
|
|
2178
|
+
}
|
|
2179
|
+
if (error instanceof QoderLlmError) {
|
|
2180
|
+
dependencies.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(error));
|
|
2181
|
+
throw error;
|
|
2182
|
+
}
|
|
2183
|
+
const failure = new QoderLlmError("Qoder transport request failed.", "TRANSPORT", { cause: error });
|
|
2184
|
+
dependencies.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(failure), { cause: redactLogValue(error) });
|
|
2185
|
+
throw failure;
|
|
2186
|
+
} finally {
|
|
2187
|
+
clearTimeout(headerTimer);
|
|
2188
|
+
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
2189
|
+
options.signal?.removeEventListener("abort", onCallerAbort);
|
|
2190
|
+
requestController.abort("request complete");
|
|
1441
2191
|
}
|
|
1442
|
-
const models = normalizeQoderModels(payload);
|
|
1443
|
-
if (models.length === 0) throw new QoderLlmError("Qoder model discovery returned no enabled models.", "EMPTY_RESPONSE");
|
|
1444
|
-
options.logger?.debug?.("[Qoder Models] Model catalog resolved", { models });
|
|
1445
|
-
return models;
|
|
1446
2192
|
}
|
|
1447
2193
|
//#endregion
|
|
1448
|
-
//#region src/
|
|
1449
|
-
const
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
function normalizeQuota(raw) {
|
|
1453
|
-
if (!raw || typeof raw !== "object") return void 0;
|
|
1454
|
-
const total = typeof raw.total === "number" && Number.isFinite(raw.total) ? raw.total : typeof raw.cap === "number" && Number.isFinite(raw.cap) ? raw.cap : typeof raw.remaining === "number" && typeof raw.used === "number" ? raw.used + raw.remaining : 0;
|
|
1455
|
-
const used = typeof raw.used === "number" && Number.isFinite(raw.used) ? raw.used : 0;
|
|
1456
|
-
const remaining = typeof raw.remaining === "number" && Number.isFinite(raw.remaining) ? raw.remaining : Math.max(0, total - used);
|
|
1457
|
-
let percentage;
|
|
1458
|
-
if (typeof raw.percentage === "number" && Number.isFinite(raw.percentage)) percentage = raw.percentage <= 1 && total > 1 ? raw.percentage * 100 : raw.percentage;
|
|
1459
|
-
else percentage = total > 0 ? used / total * 100 : 0;
|
|
1460
|
-
const unit = typeof raw.unit === "string" && raw.unit.length > 0 ? raw.unit : "credits";
|
|
1461
|
-
return {
|
|
1462
|
-
total,
|
|
1463
|
-
used,
|
|
1464
|
-
remaining,
|
|
1465
|
-
percentage,
|
|
1466
|
-
unit
|
|
1467
|
-
};
|
|
1468
|
-
}
|
|
1469
|
-
function normalizeExpiresAt(rawExpires) {
|
|
1470
|
-
if (rawExpires === void 0 || rawExpires === null) return void 0;
|
|
1471
|
-
if (typeof rawExpires === "number" && rawExpires > 0) return new Date(rawExpires).toISOString();
|
|
1472
|
-
if (typeof rawExpires === "string" && rawExpires.length > 0) {
|
|
1473
|
-
const parsed = Date.parse(rawExpires);
|
|
1474
|
-
if (!Number.isNaN(parsed) && parsed > 0) return new Date(parsed).toISOString();
|
|
1475
|
-
}
|
|
2194
|
+
//#region src/qoder/transport/default-transport.ts
|
|
2195
|
+
const defaultStreamIdleTimeoutMs = 300 * 1e3;
|
|
2196
|
+
function aborted(message) {
|
|
2197
|
+
return new QoderLlmError(message, "ABORTED");
|
|
1476
2198
|
}
|
|
1477
|
-
var
|
|
1478
|
-
authService;
|
|
1479
|
-
fetchImpl;
|
|
1480
|
-
ttlMs;
|
|
1481
|
-
timeoutMs;
|
|
1482
|
-
resolveRegion;
|
|
2199
|
+
var DefaultQoderTransport = class {
|
|
1483
2200
|
region;
|
|
2201
|
+
resolvePat;
|
|
2202
|
+
fetchImpl;
|
|
1484
2203
|
logger;
|
|
1485
|
-
|
|
2204
|
+
streamIdleTimeoutMs;
|
|
2205
|
+
responseHeaderTimeoutMs;
|
|
2206
|
+
metadataTimeoutMs;
|
|
2207
|
+
auth;
|
|
2208
|
+
usage;
|
|
2209
|
+
modelFlights = new SingleFlight();
|
|
2210
|
+
attachments;
|
|
2211
|
+
imageUploader;
|
|
2212
|
+
preserveThinking;
|
|
1486
2213
|
constructor(options) {
|
|
1487
|
-
this.authService = options.authService;
|
|
1488
|
-
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1489
|
-
this.ttlMs = options.ttlMs ?? defaultUsageTtlMs;
|
|
1490
|
-
this.timeoutMs = options.timeoutMs ?? defaultUsageTimeoutMs;
|
|
1491
|
-
this.resolveRegion = options.resolveRegion;
|
|
1492
2214
|
this.region = options.region;
|
|
2215
|
+
this.resolvePat = options.resolvePat;
|
|
2216
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1493
2217
|
this.logger = options.logger;
|
|
2218
|
+
this.streamIdleTimeoutMs = options.streamIdleTimeoutMs ?? 3e5;
|
|
2219
|
+
this.responseHeaderTimeoutMs = options.responseHeaderTimeoutMs ?? 3e4;
|
|
2220
|
+
this.metadataTimeoutMs = options.metadataTimeoutMs;
|
|
2221
|
+
this.attachments = options.attachments;
|
|
2222
|
+
this.preserveThinking = options.preserveThinking;
|
|
2223
|
+
this.auth = new QoderAuthService({
|
|
2224
|
+
fetch: this.fetchImpl,
|
|
2225
|
+
logger: this.logger,
|
|
2226
|
+
region: this.region,
|
|
2227
|
+
resolveMachineId: options.resolveMachineId
|
|
2228
|
+
});
|
|
2229
|
+
this.usage = new QoderUsageReader({
|
|
2230
|
+
authService: this.auth,
|
|
2231
|
+
fetch: this.fetchImpl,
|
|
2232
|
+
logger: this.logger,
|
|
2233
|
+
region: this.region,
|
|
2234
|
+
timeoutMs: this.metadataTimeoutMs
|
|
2235
|
+
});
|
|
2236
|
+
this.imageUploader = new QoderImageUploader({
|
|
2237
|
+
fetch: this.fetchImpl,
|
|
2238
|
+
logger: this.logger,
|
|
2239
|
+
region: this.region,
|
|
2240
|
+
...options.imageUploadTimeoutMs === void 0 ? {} : { timeoutMs: options.imageUploadTimeoutMs },
|
|
2241
|
+
...options.imageUrlCacheTtlMs === void 0 ? {} : { cacheTtlMs: options.imageUrlCacheTtlMs },
|
|
2242
|
+
refreshCredentials: async (signal) => {
|
|
2243
|
+
const pat = await this.requirePat(signal);
|
|
2244
|
+
this.auth.clear(pat);
|
|
2245
|
+
return this.auth.getCredentials(pat, signal);
|
|
2246
|
+
}
|
|
2247
|
+
});
|
|
1494
2248
|
}
|
|
1495
|
-
|
|
1496
|
-
this.
|
|
2249
|
+
stream(options, model) {
|
|
2250
|
+
return this.generate(options, model);
|
|
1497
2251
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
2252
|
+
async discoverModels(signal) {
|
|
2253
|
+
const pat = await this.requirePat(signal);
|
|
2254
|
+
const key = opaqueCredentialKey(pat);
|
|
2255
|
+
return this.modelFlights.run(key, signal, async (sharedSignal) => {
|
|
2256
|
+
const credentials = await this.auth.getCredentials(pat, sharedSignal);
|
|
2257
|
+
return retryMetadataRead(sharedSignal, () => fetchQoderModels(credentials, {
|
|
2258
|
+
fetch: this.fetchImpl,
|
|
2259
|
+
signal: sharedSignal,
|
|
2260
|
+
logger: this.logger,
|
|
2261
|
+
region: this.region,
|
|
2262
|
+
timeoutMs: this.metadataTimeoutMs
|
|
2263
|
+
}));
|
|
2264
|
+
}, () => aborted("Qoder model discovery was aborted."));
|
|
1501
2265
|
}
|
|
1502
|
-
async readAccount(
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
const cached = this.cache.get(cacheKey);
|
|
1508
|
-
if (cached && cached.expiresAt > Date.now()) return cached.info;
|
|
1509
|
-
}
|
|
1510
|
-
const creds = await this.authService.getCredentials(pat, options?.signal, effectiveRegion);
|
|
1511
|
-
const accountInfo = {
|
|
1512
|
-
profile: {
|
|
1513
|
-
id: creds.userID,
|
|
1514
|
-
name: creds.name || "Qoder User",
|
|
1515
|
-
email: creds.email || ""
|
|
1516
|
-
},
|
|
1517
|
-
usage: await this.fetchUsage(creds.authToken, options?.signal, effectiveRegion),
|
|
1518
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1519
|
-
};
|
|
1520
|
-
this.cache.set(cacheKey, {
|
|
1521
|
-
info: accountInfo,
|
|
1522
|
-
expiresAt: Date.now() + this.ttlMs
|
|
2266
|
+
async readAccount(options) {
|
|
2267
|
+
const pat = await this.requirePat(options?.signal);
|
|
2268
|
+
return this.usage.readAccount(pat, {
|
|
2269
|
+
force: options?.force,
|
|
2270
|
+
signal: options?.signal
|
|
1523
2271
|
});
|
|
1524
|
-
return accountInfo;
|
|
1525
2272
|
}
|
|
1526
|
-
|
|
1527
|
-
if (
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
this.
|
|
1549
|
-
|
|
1550
|
-
statusText: response.statusText
|
|
1551
|
-
});
|
|
1552
|
-
const text = await response.text();
|
|
1553
|
-
if (!response.ok) {
|
|
1554
|
-
this.logger?.error?.("[Qoder Usage] Request failed", redactLogPayload(text));
|
|
1555
|
-
throw qoderHttpError(`Failed to fetch Qoder quota usage with status ${response.status}: ${text}`, response);
|
|
1556
|
-
}
|
|
1557
|
-
let data;
|
|
1558
|
-
try {
|
|
1559
|
-
data = JSON.parse(text);
|
|
1560
|
-
} catch {
|
|
1561
|
-
this.logger?.error?.("[Qoder Usage] Invalid JSON response", redactLogPayload(text));
|
|
1562
|
-
throw new QoderLlmError("Failed to parse Qoder quota JSON response", "USAGE_FETCH_FAILED");
|
|
1563
|
-
}
|
|
1564
|
-
this.logger?.debug?.("[Qoder Usage] Quota usage resolved", redactLogValue(data));
|
|
1565
|
-
return {
|
|
1566
|
-
userQuota: normalizeQuota(data.userQuota),
|
|
1567
|
-
orgResourcePackage: normalizeQuota(data.orgResourcePackage),
|
|
1568
|
-
totalUsagePercentage: typeof data.totalUsagePercentage === "number" ? data.totalUsagePercentage : void 0,
|
|
1569
|
-
isQuotaExceeded: typeof data.isQuotaExceeded === "boolean" ? data.isQuotaExceeded : false,
|
|
1570
|
-
expiresAt: normalizeExpiresAt(data.expiresAt),
|
|
1571
|
-
raw: data
|
|
1572
|
-
};
|
|
1573
|
-
} catch (error) {
|
|
1574
|
-
if (error instanceof QoderLlmError) throw error;
|
|
1575
|
-
if (signal?.aborted) throw new QoderLlmError("Qoder quota usage request was aborted.", "ABORTED");
|
|
1576
|
-
if (timeoutSignal.aborted) throw new QoderLlmError("Qoder quota usage request timed out.", "TIMEOUT");
|
|
1577
|
-
throw new QoderLlmError("Qoder quota usage network request failed.", "TRANSPORT", { cause: error });
|
|
1578
|
-
}
|
|
2273
|
+
async requirePat(signal) {
|
|
2274
|
+
if (signal?.aborted) throw aborted("Qoder request was aborted.");
|
|
2275
|
+
const pat = (await this.resolvePat()).trim();
|
|
2276
|
+
if (!pat) throw new QoderLlmError("Qoder Personal Access Token is missing. Configure Qoder in the Qoder settings page.", "MISSING_CREDENTIAL");
|
|
2277
|
+
if (signal?.aborted) throw aborted("Qoder request was aborted.");
|
|
2278
|
+
return pat;
|
|
2279
|
+
}
|
|
2280
|
+
async *generate(options, model) {
|
|
2281
|
+
if (options.signal?.aborted) throw aborted("Request was aborted prior to generation.");
|
|
2282
|
+
validateQoderRequestShape(options, model);
|
|
2283
|
+
const pat = await this.requirePat(options.signal);
|
|
2284
|
+
const credentials = await this.auth.getCredentials(pat, options.signal);
|
|
2285
|
+
const messages = await translateQoderMessages(options, this.attachments, {
|
|
2286
|
+
uploader: this.imageUploader,
|
|
2287
|
+
credentials,
|
|
2288
|
+
preserveThinking: this.preserveThinking
|
|
2289
|
+
});
|
|
2290
|
+
yield* streamQoderChat(options, model, await this.auth.getCredentials(pat, options.signal), messages, {
|
|
2291
|
+
fetch: this.fetchImpl,
|
|
2292
|
+
logger: this.logger,
|
|
2293
|
+
region: this.region,
|
|
2294
|
+
responseHeaderTimeoutMs: this.responseHeaderTimeoutMs,
|
|
2295
|
+
streamIdleTimeoutMs: this.streamIdleTimeoutMs
|
|
2296
|
+
});
|
|
1579
2297
|
}
|
|
1580
2298
|
};
|
|
1581
2299
|
//#endregion
|
|
1582
|
-
//#region src/index.ts
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
const providerQoder = "qoder-official";
|
|
1590
|
-
const settingsNamespace = "provider-qoder";
|
|
1591
|
-
const qoderChannel = "/qoder-subscription";
|
|
1592
|
-
const fiberDisposed = 4;
|
|
1593
|
-
const fiberUnloading = 5;
|
|
2300
|
+
//#region src/qoder/transport/index.ts
|
|
2301
|
+
function createQoderTransport(options) {
|
|
2302
|
+
return new DefaultQoderTransport(options);
|
|
2303
|
+
}
|
|
2304
|
+
//#endregion
|
|
2305
|
+
//#region src/dsh/config.ts
|
|
2306
|
+
/** DSH configuration schema and region-scoped model catalog migration rules. */
|
|
1594
2307
|
const catalogModel = z.object({
|
|
1595
2308
|
id: z.string().required(),
|
|
1596
2309
|
name: z.string().required(),
|
|
1597
2310
|
description: z.string(),
|
|
1598
2311
|
contextWindow: z.number().step(1).min(1),
|
|
2312
|
+
maxContextWindow: z.number().step(1).min(1),
|
|
1599
2313
|
maxTokens: z.number().step(1).min(1),
|
|
1600
2314
|
source: z.string(),
|
|
1601
2315
|
isReasoning: z.boolean(),
|
|
@@ -1607,15 +2321,19 @@ const catalogModel = z.object({
|
|
|
1607
2321
|
})),
|
|
1608
2322
|
defaultReasoningEffort: z.string(),
|
|
1609
2323
|
priceFactor: z.number().min(0),
|
|
2324
|
+
supportsImages: z.boolean(),
|
|
1610
2325
|
contextOptions: z.dict(z.object({
|
|
1611
2326
|
tokenCount: z.number().step(1).min(1),
|
|
1612
2327
|
isDefault: z.boolean()
|
|
1613
2328
|
}))
|
|
1614
2329
|
});
|
|
2330
|
+
const modelsByRegionSchema = z.dict(z.array(catalogModel));
|
|
1615
2331
|
const Config = z.object({
|
|
1616
2332
|
region: z.union(["global", "china"]).default("global"),
|
|
1617
|
-
|
|
1618
|
-
|
|
2333
|
+
modelsByRegion: modelsByRegionSchema.default({}),
|
|
2334
|
+
models: z.array(catalogModel),
|
|
2335
|
+
streamIdleTimeoutMs: z.number().step(1).min(1).default(defaultStreamIdleTimeoutMs),
|
|
2336
|
+
preserveThinking: z.boolean().default(true)
|
|
1619
2337
|
});
|
|
1620
2338
|
function resolveModels(models) {
|
|
1621
2339
|
const resolved = models ?? defaultModels;
|
|
@@ -1628,6 +2346,117 @@ function resolveModels(models) {
|
|
|
1628
2346
|
return { ...model };
|
|
1629
2347
|
});
|
|
1630
2348
|
}
|
|
2349
|
+
function modelsFor(config, region, legacyModelsRegion = config.region ?? "global") {
|
|
2350
|
+
const scoped = config.modelsByRegion?.[region];
|
|
2351
|
+
if (scoped !== void 0) return resolveModels(scoped);
|
|
2352
|
+
if (legacyModelsRegion === region && config.models !== void 0 && config.models.length > 0) return resolveModels(config.models);
|
|
2353
|
+
return resolveModels(defaultModels);
|
|
2354
|
+
}
|
|
2355
|
+
//#endregion
|
|
2356
|
+
//#region src/dsh/adapter.ts
|
|
2357
|
+
/** Thin DSH adapter over the Qoder transport seam. */
|
|
2358
|
+
function modelInfo(provider, model) {
|
|
2359
|
+
return {
|
|
2360
|
+
provider,
|
|
2361
|
+
id: model.id,
|
|
2362
|
+
name: model.priceFactor === void 0 ? model.name : `${model.name} (${model.priceFactor}x)`,
|
|
2363
|
+
description: model.description,
|
|
2364
|
+
inputModalities: model.supportsImages === true ? ["text", "image"] : ["text"]
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
var QoderAdapter = class extends LlmAdapter {
|
|
2368
|
+
resolveTransport;
|
|
2369
|
+
catalogModels;
|
|
2370
|
+
providerId;
|
|
2371
|
+
providerName;
|
|
2372
|
+
constructor(options) {
|
|
2373
|
+
super();
|
|
2374
|
+
this.resolveTransport = options.resolveTransport;
|
|
2375
|
+
this.catalogModels = options.models && options.models.length > 0 ? options.models : defaultModels;
|
|
2376
|
+
this.providerId = options.providerId ?? "qoder-official";
|
|
2377
|
+
this.providerName = options.providerName ?? "Qoder";
|
|
2378
|
+
}
|
|
2379
|
+
providerInfo(provider) {
|
|
2380
|
+
return {
|
|
2381
|
+
id: provider,
|
|
2382
|
+
name: this.providerName
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
listModels(provider) {
|
|
2386
|
+
return Promise.resolve(this.catalogModels.map((model) => modelInfo(provider, model)));
|
|
2387
|
+
}
|
|
2388
|
+
replaceModels(models) {
|
|
2389
|
+
this.catalogModels = models;
|
|
2390
|
+
}
|
|
2391
|
+
resolveModel(provider, modelId, signal) {
|
|
2392
|
+
if (signal?.aborted) return Promise.reject(new QoderLlmError("Qoder model resolution was aborted.", "ABORTED"));
|
|
2393
|
+
const configured = this.catalogModels.find((model) => model.id === modelId);
|
|
2394
|
+
if (configured === void 0) return Promise.resolve({
|
|
2395
|
+
provider,
|
|
2396
|
+
id: modelId,
|
|
2397
|
+
name: modelId,
|
|
2398
|
+
inputModalities: ["text"]
|
|
2399
|
+
});
|
|
2400
|
+
return Promise.resolve({
|
|
2401
|
+
...modelInfo(provider, configured),
|
|
2402
|
+
...configured.contextWindow === void 0 ? {} : { context: { contextWindow: configured.contextWindow } },
|
|
2403
|
+
...configured.maxTokens === void 0 ? {} : { defaultMaxTokens: configured.maxTokens },
|
|
2404
|
+
...configured.reasoningEfforts === void 0 || configured.reasoningEfforts.length === 0 ? {} : { reasoning: {
|
|
2405
|
+
efforts: configured.reasoningEfforts.map((effort) => ({
|
|
2406
|
+
id: ReasoningEffortId(effort.id),
|
|
2407
|
+
name: effort.name,
|
|
2408
|
+
...effort.description === void 0 ? {} : { description: effort.description }
|
|
2409
|
+
})),
|
|
2410
|
+
...configured.isReasoning === false || configured.defaultReasoningEffort === void 0 ? {} : { defaultEffort: ReasoningEffortId(configured.defaultReasoningEffort) }
|
|
2411
|
+
} }
|
|
2412
|
+
});
|
|
2413
|
+
}
|
|
2414
|
+
stream(options) {
|
|
2415
|
+
if (options.provider !== this.providerId) throw new QoderLlmError(`Qoder adapter does not own provider "${options.provider}".`, "INVALID_PROVIDER");
|
|
2416
|
+
const model = this.catalogModels.find((candidate) => candidate.id === (options.model || "cmodel"));
|
|
2417
|
+
return this.resolveTransport().stream(options, model);
|
|
2418
|
+
}
|
|
2419
|
+
};
|
|
2420
|
+
//#endregion
|
|
2421
|
+
//#region src/dsh/credential-contract.ts
|
|
2422
|
+
/** Shared DSH host/browser contract for the one managed Qoder credential. */
|
|
2423
|
+
const qoderCredentialRef = "QODER_MANAGED_CREDENTIAL";
|
|
2424
|
+
/** DSH's local provider uses `env` and `*-env` for ambient credential layers. */
|
|
2425
|
+
function isEnvironmentCredentialSource(source) {
|
|
2426
|
+
return source === "env" || source?.endsWith("-env") === true;
|
|
2427
|
+
}
|
|
2428
|
+
//#endregion
|
|
2429
|
+
//#region src/dsh/credential.ts
|
|
2430
|
+
/** DSH host-side resolution of the one managed Qoder credential. */
|
|
2431
|
+
const managedQoderCredentialRef = credentialRef(qoderCredentialRef);
|
|
2432
|
+
/** Resolve only provider-managed storage; ambient environment layers are intentionally unsupported. */
|
|
2433
|
+
async function resolveManagedQoderPat(credentials) {
|
|
2434
|
+
const hit = await credentials.resolve(managedQoderCredentialRef);
|
|
2435
|
+
if (hit === void 0 || isEnvironmentCredentialSource(hit.source)) return "";
|
|
2436
|
+
return hit.value.trim();
|
|
2437
|
+
}
|
|
2438
|
+
//#endregion
|
|
2439
|
+
//#region src/dsh/plugin.ts
|
|
2440
|
+
const name = "provider-qoder";
|
|
2441
|
+
const inject = [
|
|
2442
|
+
"llm",
|
|
2443
|
+
"credentials",
|
|
2444
|
+
"connection",
|
|
2445
|
+
"attachments"
|
|
2446
|
+
];
|
|
2447
|
+
const providerQoder = "qoder-official";
|
|
2448
|
+
const settingsNamespace = "provider-qoder";
|
|
2449
|
+
const qoderChannel = "/qoder-subscription";
|
|
2450
|
+
const fiberDisposed = 4;
|
|
2451
|
+
const fiberUnloading = 5;
|
|
2452
|
+
function logError(error) {
|
|
2453
|
+
if (!(error instanceof Error)) return { name: "UnknownError" };
|
|
2454
|
+
return {
|
|
2455
|
+
name: error.name,
|
|
2456
|
+
message: error.message,
|
|
2457
|
+
..."code" in error ? { code: error.code } : {}
|
|
2458
|
+
};
|
|
2459
|
+
}
|
|
1631
2460
|
function publicError(message) {
|
|
1632
2461
|
return {
|
|
1633
2462
|
ok: false,
|
|
@@ -1640,81 +2469,119 @@ function publicError(message) {
|
|
|
1640
2469
|
}
|
|
1641
2470
|
function apply(ctx, config = {}) {
|
|
1642
2471
|
const logger = ctx.logger;
|
|
2472
|
+
const initialRegion = config.region ?? "global";
|
|
2473
|
+
const hasConfiguredModels = config.models !== void 0 && config.models.length > 0;
|
|
2474
|
+
const baseConfig = {
|
|
2475
|
+
region: initialRegion,
|
|
2476
|
+
modelsByRegion: { ...config.modelsByRegion },
|
|
2477
|
+
...hasConfiguredModels ? { models: resolveModels(config.models) } : {},
|
|
2478
|
+
streamIdleTimeoutMs: config.streamIdleTimeoutMs ?? 3e5,
|
|
2479
|
+
preserveThinking: config.preserveThinking ?? true
|
|
2480
|
+
};
|
|
1643
2481
|
let current = () => baseConfig;
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
2482
|
+
let legacyModelsRegion = initialRegion;
|
|
2483
|
+
let persistDiscoveredModels = async (_region) => {};
|
|
2484
|
+
const discoveredCatalogs = {
|
|
2485
|
+
global: [],
|
|
2486
|
+
china: []
|
|
2487
|
+
};
|
|
2488
|
+
const createTransport = (region, streamIdleTimeoutMs, preserveThinking, resolvePat = () => resolveManagedQoderPat(ctx.credentials)) => createQoderTransport({
|
|
2489
|
+
region,
|
|
2490
|
+
resolvePat,
|
|
1650
2491
|
logger,
|
|
1651
|
-
|
|
2492
|
+
streamIdleTimeoutMs,
|
|
2493
|
+
attachments: ctx.attachments,
|
|
2494
|
+
preserveThinking
|
|
1652
2495
|
});
|
|
1653
|
-
const
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
2496
|
+
const resolveConfig = () => {
|
|
2497
|
+
const value = current();
|
|
2498
|
+
const region = value.region ?? "global";
|
|
2499
|
+
return {
|
|
2500
|
+
region,
|
|
2501
|
+
models: mergeQoderDiscoveryMetadata(modelsFor(value, region, legacyModelsRegion), discoveredCatalogs[region]),
|
|
2502
|
+
streamIdleTimeoutMs: value.streamIdleTimeoutMs ?? 3e5,
|
|
2503
|
+
preserveThinking: value.preserveThinking ?? true
|
|
2504
|
+
};
|
|
1657
2505
|
};
|
|
1658
|
-
let discoveredCatalog = [];
|
|
1659
|
-
let previousRegion = baseConfig.region ?? "global";
|
|
1660
|
-
const resolveConfig = () => ({
|
|
1661
|
-
region: current().region ?? "global",
|
|
1662
|
-
models: mergeQoderDiscoveryMetadata(resolveModels(current().models), discoveredCatalog),
|
|
1663
|
-
streamIdleTimeoutMs: current().streamIdleTimeoutMs ?? 3e5
|
|
1664
|
-
});
|
|
1665
2506
|
const initial = resolveConfig();
|
|
2507
|
+
let activeTransport = createTransport(initial.region, initial.streamIdleTimeoutMs, initial.preserveThinking);
|
|
2508
|
+
let activeTransportConfig = {
|
|
2509
|
+
region: initial.region,
|
|
2510
|
+
streamIdleTimeoutMs: initial.streamIdleTimeoutMs,
|
|
2511
|
+
preserveThinking: initial.preserveThinking
|
|
2512
|
+
};
|
|
1666
2513
|
const adapter = new QoderAdapter({
|
|
1667
|
-
|
|
2514
|
+
resolveTransport: () => activeTransport,
|
|
1668
2515
|
models: initial.models,
|
|
1669
2516
|
providerId: providerQoder,
|
|
1670
|
-
providerName: "Qoder"
|
|
1671
|
-
authService,
|
|
1672
|
-
logger,
|
|
1673
|
-
streamIdleTimeoutMs: initial.streamIdleTimeoutMs,
|
|
1674
|
-
region: initial.region
|
|
2517
|
+
providerName: "Qoder"
|
|
1675
2518
|
});
|
|
1676
2519
|
const registration = ctx.llm.registerAdapter([providerQoder], adapter);
|
|
1677
2520
|
const refreshAdapter = () => {
|
|
1678
2521
|
const next = resolveConfig();
|
|
1679
|
-
if (next.region !==
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
2522
|
+
if (next.region !== activeTransportConfig.region || next.streamIdleTimeoutMs !== activeTransportConfig.streamIdleTimeoutMs || next.preserveThinking !== activeTransportConfig.preserveThinking) {
|
|
2523
|
+
activeTransport = createTransport(next.region, next.streamIdleTimeoutMs, next.preserveThinking);
|
|
2524
|
+
activeTransportConfig = {
|
|
2525
|
+
region: next.region,
|
|
2526
|
+
streamIdleTimeoutMs: next.streamIdleTimeoutMs,
|
|
2527
|
+
preserveThinking: next.preserveThinking
|
|
2528
|
+
};
|
|
1683
2529
|
}
|
|
1684
|
-
adapter.
|
|
2530
|
+
adapter.replaceModels(next.models);
|
|
1685
2531
|
registration.replace([providerQoder]);
|
|
1686
2532
|
};
|
|
1687
2533
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
1688
2534
|
const scope = settingsCtx.settings.register(settingsNamespace, Config, {
|
|
1689
2535
|
base: baseConfig,
|
|
1690
2536
|
validate: (value) => {
|
|
1691
|
-
|
|
2537
|
+
for (const region of Object.keys(value.modelsByRegion ?? {})) if (region !== "global" && region !== "china") throw new Error(`provider-qoder: unsupported model catalog region "${region}"`);
|
|
2538
|
+
modelsFor(value, "global", legacyModelsRegion);
|
|
2539
|
+
modelsFor(value, "china", legacyModelsRegion);
|
|
1692
2540
|
}
|
|
1693
2541
|
});
|
|
2542
|
+
legacyModelsRegion = scope.get().region ?? initialRegion;
|
|
1694
2543
|
current = () => scope.get();
|
|
2544
|
+
persistDiscoveredModels = async (region) => {
|
|
2545
|
+
const value = scope.get();
|
|
2546
|
+
const selected = modelsFor(value, region, legacyModelsRegion);
|
|
2547
|
+
const enriched = mergeQoderDiscoveryMetadata(selected, discoveredCatalogs[region]);
|
|
2548
|
+
if (!hasSameQoderDiscoveryMetadata(selected, enriched)) await scope.update({ modelsByRegion: {
|
|
2549
|
+
...value.modelsByRegion,
|
|
2550
|
+
[region]: enriched
|
|
2551
|
+
} });
|
|
2552
|
+
};
|
|
1695
2553
|
refreshAdapter();
|
|
2554
|
+
const loaded = scope.get();
|
|
2555
|
+
const loadedRegion = loaded.region ?? "global";
|
|
2556
|
+
if (loaded.models !== void 0 && loaded.models.length > 0 && loaded.modelsByRegion?.[loadedRegion] === void 0) scope.update({ modelsByRegion: {
|
|
2557
|
+
...loaded.modelsByRegion,
|
|
2558
|
+
[loadedRegion]: resolveModels(loaded.models)
|
|
2559
|
+
} }).catch((error) => logger?.error?.("[Qoder Settings] Failed to migrate model catalog", logError(error)));
|
|
1696
2560
|
scope.watch(async (next) => {
|
|
1697
2561
|
if (ctx.fiber.state === fiberUnloading || ctx.fiber.state === fiberDisposed) return;
|
|
1698
2562
|
refreshAdapter();
|
|
1699
|
-
const
|
|
1700
|
-
|
|
2563
|
+
const region = next.region ?? "global";
|
|
2564
|
+
const selected = modelsFor(next, region, legacyModelsRegion);
|
|
2565
|
+
const enriched = mergeQoderDiscoveryMetadata(selected, discoveredCatalogs[region]);
|
|
2566
|
+
if (!hasSameQoderDiscoveryMetadata(selected, enriched)) await scope.update({ modelsByRegion: {
|
|
2567
|
+
...next.modelsByRegion,
|
|
2568
|
+
[region]: enriched
|
|
2569
|
+
} });
|
|
1701
2570
|
});
|
|
1702
2571
|
settingsCtx.effect(() => () => {
|
|
1703
2572
|
if (ctx.fiber.state === fiberUnloading || ctx.fiber.state === fiberDisposed) return;
|
|
2573
|
+
persistDiscoveredModels = async () => {};
|
|
1704
2574
|
current = () => baseConfig;
|
|
1705
2575
|
refreshAdapter();
|
|
1706
2576
|
});
|
|
1707
2577
|
});
|
|
1708
2578
|
const discoverModels = async (signal, suppliedPat) => {
|
|
1709
|
-
const
|
|
1710
|
-
|
|
1711
|
-
const
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
region
|
|
1716
|
-
});
|
|
1717
|
-
discoveredCatalog = models;
|
|
2579
|
+
const snapshot = resolveConfig();
|
|
2580
|
+
const normalizedPat = suppliedPat?.trim();
|
|
2581
|
+
const models = await (normalizedPat ? createTransport(snapshot.region, snapshot.streamIdleTimeoutMs, snapshot.preserveThinking, () => Promise.resolve(normalizedPat)) : activeTransport).discoverModels(signal);
|
|
2582
|
+
discoveredCatalogs[snapshot.region] = models;
|
|
2583
|
+
refreshAdapter();
|
|
2584
|
+
await persistDiscoveredModels(snapshot.region);
|
|
1718
2585
|
return models;
|
|
1719
2586
|
};
|
|
1720
2587
|
ctx.llm.registerModelDiscovery(settingsNamespace, async (request, signal) => {
|
|
@@ -1737,28 +2604,15 @@ function apply(ctx, config = {}) {
|
|
|
1737
2604
|
};
|
|
1738
2605
|
} catch (error) {
|
|
1739
2606
|
if (signal.aborted || error instanceof QoderLlmError && error.code === "ABORTED") throw error;
|
|
1740
|
-
logger?.error?.("[Qoder RPC] Failed to discover models",
|
|
2607
|
+
logger?.error?.("[Qoder RPC] Failed to discover models", logError(error));
|
|
1741
2608
|
return publicError(error instanceof Error ? error.message : "Failed to discover Qoder models");
|
|
1742
2609
|
}
|
|
1743
2610
|
const force = typeof payload === "object" && payload !== null && "force" in payload ? payload.force === true : false;
|
|
1744
2611
|
logger?.debug?.("[Qoder RPC] Reading subscriber account", { force });
|
|
1745
|
-
let pat;
|
|
1746
|
-
try {
|
|
1747
|
-
pat = await resolveManagedQoderPat(ctx.credentials);
|
|
1748
|
-
} catch (error) {
|
|
1749
|
-
if (signal.aborted) throw error;
|
|
1750
|
-
logger?.error?.("[Qoder RPC] Failed to resolve managed PAT", redactLogValue(error));
|
|
1751
|
-
return publicError(error instanceof Error ? error.message : "No Qoder PAT configured");
|
|
1752
|
-
}
|
|
1753
|
-
if (!pat) {
|
|
1754
|
-
logger?.warn?.("[Qoder RPC] No managed Qoder PAT is configured");
|
|
1755
|
-
return publicError("Qoder PAT is not configured");
|
|
1756
|
-
}
|
|
1757
2612
|
try {
|
|
1758
|
-
const account = await
|
|
2613
|
+
const account = await activeTransport.readAccount({
|
|
1759
2614
|
force,
|
|
1760
|
-
signal
|
|
1761
|
-
region: current().region ?? "global"
|
|
2615
|
+
signal
|
|
1762
2616
|
});
|
|
1763
2617
|
logger?.debug?.("[Qoder RPC] Subscriber account resolved");
|
|
1764
2618
|
return {
|
|
@@ -1767,7 +2621,7 @@ function apply(ctx, config = {}) {
|
|
|
1767
2621
|
};
|
|
1768
2622
|
} catch (error) {
|
|
1769
2623
|
if (signal.aborted || error instanceof QoderLlmError && error.code === "ABORTED") throw error;
|
|
1770
|
-
logger?.error?.("[Qoder RPC] Failed to read subscriber account",
|
|
2624
|
+
logger?.error?.("[Qoder RPC] Failed to read subscriber account", logError(error));
|
|
1771
2625
|
return publicError(error instanceof Error ? error.message : "Failed to load Qoder account");
|
|
1772
2626
|
}
|
|
1773
2627
|
};
|
|
@@ -1775,4 +2629,4 @@ function apply(ctx, config = {}) {
|
|
|
1775
2629
|
}
|
|
1776
2630
|
}
|
|
1777
2631
|
//#endregion
|
|
1778
|
-
export { Config,
|
|
2632
|
+
export { Config, apply, inject, name };
|