dsh-provider-qoder 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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/endpoints.ts
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
- const qoderGlobalBaseUrl = qoderRegionEndpoints.global.baseUrl;
20
- const qoderGlobalOpenApiUrl = qoderRegionEndpoints.global.openApiUrl;
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/logging.ts
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
- //#endregion
126
- //#region src/machine-id.ts
127
- function defaultMachineIdPaths() {
128
- return [join(homedir(), ".qoder", ".auth", "machine_id"), join(homedir(), ".dsh", "qoder", "machine_id")];
129
- }
130
- /** Read Qoder's machine id or create the DSH-owned fallback. */
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/auth.ts
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.3";
351
- const qoderClientType = "5";
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": qoderClientType,
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,421 @@ 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/encoding.ts
426
- /**
427
- * Qoder WAF Body encoding algorithm.
428
- *
429
- * Re-arranges standard Base64 chunks and translates characters against
430
- * a custom alphabet to satisfy Qoder upstream WAF requirements.
431
- *
432
- * @module dsh-provider-qoder/encoding
433
- */
434
- const qoderCustomAlphabet = "_doRTgHZBKcGVjlvpC,@aFSx#DPuNJme&i*MzLOEn)sUrthbf%Y^w.(kIQyXqWA!";
435
- const qoderStdAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
436
- function qoderEncodeBody(plaintext) {
437
- const std = Buffer.isBuffer(plaintext) ? plaintext.toString("base64") : Buffer.from(plaintext).toString("base64");
438
- const n = std.length;
439
- const a = Math.floor(n / 3);
440
- const rearranged = std.slice(n - a) + std.slice(a, n - a) + std.slice(0, a);
441
- let out = "";
442
- for (let i = 0; i < n; i++) {
443
- const c = rearranged[i];
444
- if (c === "=") out += "$";
445
- else {
446
- const idx = qoderStdAlphabet.indexOf(c);
447
- if (idx >= 0) out += qoderCustomAlphabet[idx];
448
- else out += c;
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") {
827
+ if (message.role !== "assistant") throw unsupported("Qoder historical reasoning is valid only in assistant messages.");
828
+ continue;
829
+ }
830
+ throw unsupported(`Qoder transport encountered unsupported block type: ${String(block.type)}`);
831
+ }
832
+ }
833
+ }
834
+ /** Reject a batch that exceeds the deployment image policy before any upload work starts. */
835
+ function enforceImageLimits(images, attachments) {
836
+ const limits = attachments.imageLimits;
837
+ if (images.length > limits.maxImagesPerMessage) throw unsupported(`Qoder accepts at most ${limits.maxImagesPerMessage} images per message; received ${images.length}.`);
838
+ let total = 0;
839
+ for (const image of images) total += image.attachment.bytes;
840
+ if (total > limits.maxMessageImageBytes) throw unsupported("Qoder message image content exceeds the configured total byte limit.");
841
+ }
842
+ async function resolveImagePart(block, context) {
843
+ const { attachments, uploader, credentials, signal } = context;
844
+ if (attachments === void 0) throw new QoderLlmError("Qoder image input requires the DSH attachment service.", "ATTACHMENT");
845
+ let image;
846
+ try {
847
+ const limits = attachments.imageLimits;
848
+ image = await attachments.readImageRequest(block.attachment, {
849
+ maxPixels: limits.maxImagePixels,
850
+ maxBytes: limits.maxImageBytes
851
+ }, signal);
852
+ } catch (error) {
853
+ if (signal?.aborted) throw new QoderLlmError("Qoder image preparation was aborted.", "ABORTED", { cause: error });
854
+ if (error instanceof QoderLlmError) throw error;
855
+ throw new QoderLlmError("Qoder could not prepare an image attachment.", "ATTACHMENT", { cause: error });
856
+ }
857
+ if (uploader !== void 0 && credentials !== void 0) return {
858
+ type: "image_url",
859
+ image_url: { url: await uploader.resolveImageUrl(image, credentials, signal) }
860
+ };
861
+ return {
862
+ type: "image_url",
863
+ image_url: { url: `data:${image.mediaType};base64,${Buffer.from(image.data).toString("base64")}` }
864
+ };
865
+ }
466
866
  function translateTools(tools) {
467
867
  return (tools ?? []).map((tool) => ({
468
868
  type: "function",
@@ -473,7 +873,14 @@ function translateTools(tools) {
473
873
  }
474
874
  }));
475
875
  }
476
- function validateAndTranslateMessages(messages, systemPrompt) {
876
+ async function validateAndTranslateMessages(messages, systemPrompt, attachments, signal, pipeline) {
877
+ validateMessageShapes(messages);
878
+ const context = {
879
+ attachments,
880
+ signal,
881
+ uploader: pipeline?.uploader,
882
+ credentials: pipeline?.credentials
883
+ };
477
884
  const output = [];
478
885
  if (typeof systemPrompt === "string" && systemPrompt.trim().length > 0) output.push({
479
886
  role: "system",
@@ -483,23 +890,50 @@ function validateAndTranslateMessages(messages, systemPrompt) {
483
890
  const toolResults = message.content.filter((block) => block.type === "tool-result");
484
891
  if (toolResults.length > 0) {
485
892
  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) output.push({
487
- role: "tool",
488
- tool_call_id: String(result.toolCallId),
489
- content: toolResultText(result)
490
- });
893
+ for (const result of toolResults) {
894
+ output.push({
895
+ role: "tool",
896
+ tool_call_id: String(result.toolCallId),
897
+ content: toolResultText(result)
898
+ });
899
+ const images = result.content.filter((block) => block.type === "image");
900
+ if (images.length > 0) {
901
+ if (attachments !== void 0) enforceImageLimits(images, attachments);
902
+ output.push({
903
+ role: "user",
904
+ content: [{
905
+ type: "text",
906
+ text: `[${images.length} image${images.length === 1 ? "" : "s"} returned by the previous tool call]`
907
+ }, ...await Promise.all(images.map((image) => resolveImagePart(image, context)))]
908
+ });
909
+ }
910
+ }
491
911
  continue;
492
912
  }
493
913
  let text = "";
914
+ const userContent = [];
915
+ const pendingImages = [];
916
+ let hasImage = false;
494
917
  const toolCalls = [];
495
918
  for (const block of message.content) {
496
919
  if (block.type === "text") {
497
920
  text += block.text;
921
+ if (message.role === "user") userContent.push({
922
+ type: "text",
923
+ text: block.text
924
+ });
925
+ continue;
926
+ }
927
+ if (block.type === "image") {
928
+ hasImage = true;
929
+ pendingImages.push({
930
+ slot: userContent.length,
931
+ block
932
+ });
933
+ userContent.push(void 0);
498
934
  continue;
499
935
  }
500
- if (block.type === "image") throw unsupported("Qoder transport does not support image content.");
501
936
  if (block.type === "tool-call") {
502
- if (message.role !== "assistant") throw unsupported("Qoder tool calls are valid only in assistant messages.");
503
937
  toolCalls.push({
504
938
  id: String(block.id),
505
939
  type: "function",
@@ -510,12 +944,7 @@ function validateAndTranslateMessages(messages, systemPrompt) {
510
944
  });
511
945
  continue;
512
946
  }
513
- if (block.type === "tool-result") throw unsupported("Qoder encountered an invalid mixed tool-result message.");
514
- if (block.type === "reasoning") {
515
- if (message.role !== "assistant") throw unsupported("Qoder historical reasoning is valid only in assistant messages.");
516
- continue;
517
- }
518
- throw unsupported(`Qoder transport encountered unsupported block type: ${String(block.type)}`);
947
+ if (block.type === "reasoning") continue;
519
948
  }
520
949
  if (message.role === "assistant") {
521
950
  if (!text && toolCalls.length === 0) continue;
@@ -526,15 +955,21 @@ function validateAndTranslateMessages(messages, systemPrompt) {
526
955
  });
527
956
  continue;
528
957
  }
958
+ if (pendingImages.length > 0) {
959
+ if (attachments !== void 0) enforceImageLimits(pendingImages.map((pending) => pending.block), attachments);
960
+ await Promise.all(pendingImages.map(async (pending) => {
961
+ userContent[pending.slot] = await resolveImagePart(pending.block, context);
962
+ }));
963
+ }
529
964
  output.push({
530
965
  role: message.role,
531
- content: text
966
+ content: hasImage ? userContent.filter((part) => part !== void 0) : text
532
967
  });
533
968
  }
534
969
  return output;
535
970
  }
536
971
  //#endregion
537
- //#region src/serialize.ts
972
+ //#region src/qoder/transport/wire/serialize.ts
538
973
  /** Build the minimum qodercli request envelope for a validated DSH request. */
539
974
  function stableHash(prefix, ...inputs) {
540
975
  const hash = crypto.createHash("sha256");
@@ -558,28 +993,46 @@ function stableChatRecordId(model, messages, tools, maxTokens) {
558
993
  hash.update(`mt=${maxTokens}`);
559
994
  return hash.digest("hex").slice(0, 16);
560
995
  }
561
- function validateQoderRequest(options, model) {
996
+ /**
997
+ * Reject an unusable request before any credential resolution or provider I/O.
998
+ *
999
+ * Image publication needs credentials, so message translation now runs after
1000
+ * authentication. This static pass preserves the guarantee that a request the
1001
+ * provider cannot serve never reaches the network.
1002
+ */
1003
+ function validateQoderRequestShape(options, model) {
562
1004
  if (options.reasoningEffort !== void 0) {
563
1005
  const effort = String(options.reasoningEffort);
564
1006
  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
1007
  }
566
- return validateAndTranslateMessages(options.messages, options.system);
1008
+ 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");
1009
+ validateMessageShapes(options.messages);
1010
+ }
1011
+ /** Translate a request whose shape has already been validated. */
1012
+ function translateQoderMessages(options, attachments, pipeline) {
1013
+ return validateAndTranslateMessages(options.messages, options.system, attachments, options.signal, pipeline);
1014
+ }
1015
+ async function validateQoderRequest(options, model, attachments) {
1016
+ validateQoderRequestShape(options, model);
1017
+ return validateAndTranslateMessages(options.messages, options.system, attachments, options.signal);
567
1018
  }
568
- function buildQoderRequestBody(options, userId, translatedMessages, model) {
1019
+ async function buildQoderRequestBody(options, userId, translatedMessages, model, attachments) {
569
1020
  if (!userId) throw new QoderLlmError("Qoder request identity is missing.", "AUTH");
570
1021
  const modelKey = options.model || "cmodel";
571
- const messages = translatedMessages ?? validateQoderRequest(options, model);
1022
+ const messages = translatedMessages ?? await validateQoderRequest(options, model, attachments);
572
1023
  const modelMaxTokens = model?.maxTokens ?? 32768;
573
1024
  const maxTokens = Math.min(options.maxTokens ?? modelMaxTokens, modelMaxTokens);
574
- const isReasoning = model?.isReasoning ?? false;
1025
+ const isReasoning = options.reasoningEffort !== void 0 || (model?.isReasoning ?? false);
575
1026
  const tools = translateTools(options.tools);
576
- const contextConfig = model?.contextOptions === void 0 ? void 0 : Object.fromEntries(Object.entries(model.contextOptions).map(([key, value]) => [key, {
1027
+ const defaultContexts = Object.values(model?.contextOptions ?? {}).filter((option) => option.isDefault === true && typeof option.tokenCount === "number" && Number.isFinite(option.tokenCount) && option.tokenCount > 0);
1028
+ const contextConfig = model?.contextOptions === void 0 || defaultContexts.length !== 1 ? void 0 : Object.fromEntries(Object.entries(model.contextOptions).map(([key, value]) => [key, {
577
1029
  ...value.tokenCount === void 0 ? {} : { token_count: value.tokenCount },
578
1030
  ...value.isDefault === void 0 ? {} : { is_default: value.isDefault }
579
1031
  }]));
580
1032
  let lastUserText = "";
581
1033
  for (let index = messages.length - 1; index >= 0; index--) if (messages[index].role === "user") {
582
- lastUserText = messages[index].content ?? "";
1034
+ const content = messages[index].content;
1035
+ lastUserText = typeof content === "string" ? content : Array.isArray(content) ? content.filter((part) => part.type === "text").map((part) => part.text).join("") : "";
583
1036
  break;
584
1037
  }
585
1038
  const stablePart = stableHash("qoder-session", userId, modelKey);
@@ -640,10 +1093,570 @@ function buildQoderRequestBody(options, userId, translatedMessages, model) {
640
1093
  name: lastUserText.substring(0, 30),
641
1094
  begin_at: Date.now()
642
1095
  }
643
- };
1096
+ };
1097
+ }
1098
+ const mediaTypeExtensions = {
1099
+ "image/png": "png",
1100
+ "image/jpeg": "jpg",
1101
+ "image/webp": "webp",
1102
+ "image/gif": "gif"
1103
+ };
1104
+ function aborted$2() {
1105
+ return new QoderLlmError("Qoder image upload was aborted.", "ABORTED");
1106
+ }
1107
+ /** Encode one image as a single-field multipart payload using the Qoder boundary shape. */
1108
+ function buildQoderImageMultipart(data, mediaType, boundaryId = crypto.randomUUID()) {
1109
+ const boundary = `----qodercli-${boundaryId}`;
1110
+ const extension = mediaTypeExtensions[mediaType] ?? "png";
1111
+ const header = Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="image.${extension}"\r\nContent-Type: ${mediaType}\r\n\r\n`, "utf8");
1112
+ const footer = Buffer.from(`\r\n--${boundary}--\r\n`, "utf8");
1113
+ return {
1114
+ body: Buffer.concat([
1115
+ header,
1116
+ Buffer.from(data),
1117
+ footer
1118
+ ]),
1119
+ boundary
1120
+ };
1121
+ }
1122
+ /**
1123
+ * Extract the durable object URL from a center upload response.
1124
+ *
1125
+ * @param payload - parsed response body.
1126
+ * @returns the first usable absolute URL, or undefined when none is present.
1127
+ */
1128
+ function readQoderImageUrl(payload) {
1129
+ if (payload === null || typeof payload !== "object") return void 0;
1130
+ const root = payload;
1131
+ const result = root.result;
1132
+ const data = root.data;
1133
+ const candidates = [
1134
+ root.url,
1135
+ result?.url,
1136
+ result?.oss_url,
1137
+ data?.url,
1138
+ data?.oss_url
1139
+ ];
1140
+ for (const candidate of candidates) {
1141
+ if (typeof candidate !== "string") continue;
1142
+ const trimmed = candidate.trim();
1143
+ if (trimmed.length === 0) continue;
1144
+ if (!URL.canParse(trimmed)) continue;
1145
+ return trimmed;
1146
+ }
1147
+ }
1148
+ function dataUrl(image) {
1149
+ return `data:${image.mediaType};base64,${Buffer.from(image.data).toString("base64")}`;
1150
+ }
1151
+ /**
1152
+ * Publishes request images to the Qoder center service and remembers the result.
1153
+ *
1154
+ * Publication never fails a model request: any upstream problem degrades to an
1155
+ * inline data URL so the turn proceeds with the same content it would have
1156
+ * carried before the pipeline existed.
1157
+ */
1158
+ var QoderImageUploader = class {
1159
+ cache = /* @__PURE__ */ new Map();
1160
+ flights = new SingleFlight();
1161
+ fetchImpl;
1162
+ region;
1163
+ logger;
1164
+ timeoutMs;
1165
+ cacheTtlMs;
1166
+ cacheCapacity;
1167
+ maxConcurrency;
1168
+ refreshCredentials;
1169
+ now;
1170
+ active = 0;
1171
+ queue = [];
1172
+ constructor(options = {}) {
1173
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
1174
+ this.region = options.region ?? "global";
1175
+ this.logger = options.logger;
1176
+ this.timeoutMs = options.timeoutMs ?? 3e4;
1177
+ this.cacheTtlMs = options.cacheTtlMs ?? 18e5;
1178
+ this.cacheCapacity = options.cacheCapacity ?? 512;
1179
+ this.maxConcurrency = options.maxConcurrency ?? 4;
1180
+ this.refreshCredentials = options.refreshCredentials;
1181
+ this.now = options.now ?? Date.now;
1182
+ }
1183
+ /**
1184
+ * Resolve the wire URL for one request image.
1185
+ *
1186
+ * @param image - request-encoded image produced by the DSH attachment store.
1187
+ * @param credentials - credentials authorizing the center exchange.
1188
+ * @param signal - optional caller cancellation.
1189
+ * @returns a center object URL, or an inline data URL when publication fails.
1190
+ * @throws QoderLlmError with code `ABORTED` only when the caller cancels.
1191
+ */
1192
+ async resolveImageUrl(image, credentials, signal) {
1193
+ if (signal?.aborted) throw aborted$2();
1194
+ const key = this.cacheKey(image, credentials);
1195
+ const cached = this.cache.get(key);
1196
+ if (cached !== void 0) {
1197
+ if (cached.expiresAt > this.now()) {
1198
+ this.cache.delete(key);
1199
+ this.cache.set(key, cached);
1200
+ this.logger?.debug?.("[image-upload] cache hit", {
1201
+ url: cached.url,
1202
+ region: this.region,
1203
+ mediaType: image.mediaType,
1204
+ bytes: image.data.byteLength
1205
+ });
1206
+ return cached.url;
1207
+ }
1208
+ this.cache.delete(key);
1209
+ }
1210
+ return this.flights.run(key, signal, async (sharedSignal) => {
1211
+ const url = await this.publish(image, credentials, sharedSignal);
1212
+ if (url !== void 0) {
1213
+ this.remember(key, url);
1214
+ return url;
1215
+ }
1216
+ return dataUrl(image);
1217
+ }, aborted$2);
1218
+ }
1219
+ /**
1220
+ * Derive the deduplication identity for one request image.
1221
+ *
1222
+ * `variantId` is the attachment store's deterministic cache and upload index
1223
+ * over the attachment, the request policy, and the encoder parameters, so it
1224
+ * identifies these exact bytes without rehashing them. Region and subscriber
1225
+ * are included because a center object is never shared across either.
1226
+ */
1227
+ cacheKey(image, credentials) {
1228
+ const hash = crypto.createHash("sha256");
1229
+ hash.update(getQoderImageUploadUrl(this.region));
1230
+ hash.update("\0");
1231
+ hash.update(credentials.userID);
1232
+ hash.update("\0");
1233
+ hash.update(image.mediaType);
1234
+ hash.update("\0");
1235
+ const variantId = typeof image.variantId === "string" ? image.variantId : "";
1236
+ if (variantId.length > 0) hash.update(variantId);
1237
+ else hash.update(crypto.createHash("sha256").update(Buffer.from(image.data)).digest("hex"));
1238
+ return hash.digest("hex");
1239
+ }
1240
+ remember(key, url) {
1241
+ this.cache.delete(key);
1242
+ this.cache.set(key, {
1243
+ url,
1244
+ expiresAt: this.now() + this.cacheTtlMs
1245
+ });
1246
+ while (this.cache.size > this.cacheCapacity) {
1247
+ const oldest = this.cache.keys().next();
1248
+ if (oldest.done === true) break;
1249
+ this.cache.delete(oldest.value);
1250
+ }
1251
+ }
1252
+ async withSlot(signal, operation) {
1253
+ if (signal.aborted) throw aborted$2();
1254
+ if (this.active >= this.maxConcurrency) {
1255
+ this.logger?.debug?.("[image-upload] queued", {
1256
+ region: this.region,
1257
+ active: this.active,
1258
+ queued: this.queue.length + 1
1259
+ });
1260
+ await new Promise((resolve, reject) => {
1261
+ const onAbort = () => {
1262
+ const index = this.queue.indexOf(release);
1263
+ if (index !== -1) this.queue.splice(index, 1);
1264
+ reject(aborted$2());
1265
+ };
1266
+ const release = () => {
1267
+ signal.removeEventListener("abort", onAbort);
1268
+ resolve();
1269
+ };
1270
+ signal.addEventListener("abort", onAbort, { once: true });
1271
+ this.queue.push(release);
1272
+ });
1273
+ } else this.active++;
1274
+ try {
1275
+ if (signal.aborted) throw aborted$2();
1276
+ return await operation();
1277
+ } finally {
1278
+ const next = this.queue.shift();
1279
+ if (next !== void 0) next();
1280
+ else this.active--;
1281
+ }
1282
+ }
1283
+ /** Attempt publication, returning undefined when the request must degrade. */
1284
+ async publish(image, credentials, signal) {
1285
+ return this.withSlot(signal, async () => {
1286
+ const first = await this.attempt(image, credentials, signal);
1287
+ if (first.url !== void 0) return first.url;
1288
+ if (!first.retryable || this.refreshCredentials === void 0) {
1289
+ this.warnDegraded(first.reason);
1290
+ return;
1291
+ }
1292
+ let refreshed;
1293
+ this.logger?.debug?.("[image-upload] refreshing credentials before retry", {
1294
+ region: this.region,
1295
+ reason: first.reason
1296
+ });
1297
+ try {
1298
+ refreshed = await this.refreshCredentials(signal);
1299
+ } catch (error) {
1300
+ if (signal.aborted) throw aborted$2();
1301
+ this.warnDegraded("credential refresh failed", error);
1302
+ return;
1303
+ }
1304
+ const second = await this.attempt(image, refreshed, signal);
1305
+ if (second.url !== void 0) return second.url;
1306
+ this.warnDegraded(second.reason);
1307
+ });
1308
+ }
1309
+ warnDegraded(reason, cause) {
1310
+ this.logger?.warn?.("[image-upload] upload failed, keeping base64 image", {
1311
+ region: this.region,
1312
+ reason
1313
+ }, ...cause === void 0 ? [] : [redactLogValue(cause)]);
1314
+ }
1315
+ async attempt(image, credentials, signal) {
1316
+ const requestId = crypto.randomUUID();
1317
+ const url = getQoderImageUploadUrl(this.region, requestId);
1318
+ const multipart = buildQoderImageMultipart(image.data, image.mediaType);
1319
+ const signedBody = Buffer.from(String(multipart.body.length), "utf8");
1320
+ const timeout = AbortSignal.timeout(this.timeoutMs);
1321
+ const requestSignal = AbortSignal.any([signal, timeout]);
1322
+ const startedAt = this.now();
1323
+ const details = {
1324
+ requestId,
1325
+ region: this.region,
1326
+ mediaType: image.mediaType,
1327
+ bytes: image.data.byteLength,
1328
+ timeoutMs: this.timeoutMs
1329
+ };
1330
+ this.logger?.debug?.("[image-upload] started", details);
1331
+ try {
1332
+ const response = await this.fetchImpl(url, {
1333
+ method: "PUT",
1334
+ headers: {
1335
+ ...buildAuthHeaders(signedBody, url, credentials),
1336
+ "AI-CLIENT-TIMESTAMP": String(Math.floor(this.now() / 1e3)),
1337
+ "Content-Type": `multipart/form-data; boundary=${multipart.boundary}`,
1338
+ "Content-Length": String(multipart.body.length),
1339
+ "accept": "application/json"
1340
+ },
1341
+ body: new Uint8Array(multipart.body),
1342
+ signal: requestSignal
1343
+ });
1344
+ this.logger?.debug?.("[image-upload] response received", {
1345
+ requestId,
1346
+ status: response.status,
1347
+ elapsedMs: this.now() - startedAt
1348
+ });
1349
+ const text = await readLimitedText(response, defaultMaxJsonBytes, "Qoder image upload response");
1350
+ if (!response.ok) return {
1351
+ retryable: response.status === 401 || response.status === 403,
1352
+ reason: `HTTP ${response.status}`
1353
+ };
1354
+ let payload;
1355
+ try {
1356
+ payload = JSON.parse(text);
1357
+ } catch {
1358
+ return {
1359
+ retryable: false,
1360
+ reason: "invalid JSON response"
1361
+ };
1362
+ }
1363
+ const resolved = readQoderImageUrl(payload);
1364
+ if (resolved === void 0) return {
1365
+ retryable: false,
1366
+ reason: "response carried no image URL"
1367
+ };
1368
+ this.logger?.debug?.("[image-upload] succeeded", {
1369
+ ...details,
1370
+ status: response.status,
1371
+ elapsedMs: this.now() - startedAt,
1372
+ url: resolved
1373
+ });
1374
+ return {
1375
+ url: resolved,
1376
+ retryable: false,
1377
+ reason: ""
1378
+ };
1379
+ } catch (error) {
1380
+ const failureDetails = {
1381
+ requestId,
1382
+ region: this.region,
1383
+ elapsedMs: this.now() - startedAt
1384
+ };
1385
+ if (signal.aborted) {
1386
+ this.logger?.debug?.("[image-upload] aborted", failureDetails);
1387
+ throw aborted$2();
1388
+ }
1389
+ if (timeout.aborted) {
1390
+ this.logger?.debug?.("[image-upload] timed out", failureDetails);
1391
+ return {
1392
+ retryable: false,
1393
+ reason: "upload timed out"
1394
+ };
1395
+ }
1396
+ this.logger?.debug?.("[image-upload] network failure", failureDetails, redactLogValue(error));
1397
+ return {
1398
+ retryable: false,
1399
+ reason: "network failure"
1400
+ };
1401
+ }
1402
+ }
1403
+ };
1404
+ //#endregion
1405
+ //#region src/qoder/transport/account-reader.ts
1406
+ const defaultUsageTtlMs = 6e4;
1407
+ const defaultUsageTimeoutMs = 15e3;
1408
+ function normalizeQuota(raw) {
1409
+ if (!raw || typeof raw !== "object") return void 0;
1410
+ 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;
1411
+ const used = typeof raw.used === "number" && Number.isFinite(raw.used) ? raw.used : 0;
1412
+ const remaining = typeof raw.remaining === "number" && Number.isFinite(raw.remaining) ? raw.remaining : Math.max(0, total - used);
1413
+ let percentage;
1414
+ if (typeof raw.percentage === "number" && Number.isFinite(raw.percentage)) percentage = raw.percentage <= 1 && total > 1 ? raw.percentage * 100 : raw.percentage;
1415
+ else percentage = total > 0 ? used / total * 100 : 0;
1416
+ const unit = typeof raw.unit === "string" && raw.unit.length > 0 ? raw.unit : "credits";
1417
+ return {
1418
+ total,
1419
+ used,
1420
+ remaining,
1421
+ percentage,
1422
+ unit
1423
+ };
1424
+ }
1425
+ function normalizeExpiresAt(rawExpires) {
1426
+ if (rawExpires === void 0 || rawExpires === null) return void 0;
1427
+ if (typeof rawExpires === "number" && rawExpires > 0) return new Date(rawExpires).toISOString();
1428
+ if (typeof rawExpires === "string" && rawExpires.length > 0) {
1429
+ const parsed = Date.parse(rawExpires);
1430
+ if (!Number.isNaN(parsed) && parsed > 0) return new Date(parsed).toISOString();
1431
+ }
1432
+ }
1433
+ function asString(value) {
1434
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
1435
+ }
1436
+ function asBoolean(value) {
1437
+ return typeof value === "boolean" ? value : void 0;
1438
+ }
1439
+ function asNumber(value) {
1440
+ if (typeof value === "number" && Number.isFinite(value)) return value;
1441
+ if (typeof value === "string" && value.trim().length > 0) {
1442
+ const num = Number(value);
1443
+ if (Number.isFinite(num)) return num;
1444
+ }
1445
+ }
1446
+ function normalizeOrganization(raw) {
1447
+ if (!raw || typeof raw !== "object") return void 0;
1448
+ const obj = raw;
1449
+ const orgId = asString(obj.org_id) ?? asString(obj.orgId) ?? asString(obj.id);
1450
+ const orgName = asString(obj.org_name) ?? asString(obj.orgName) ?? asString(obj.name);
1451
+ if (!orgId || !orgName) return void 0;
1452
+ return {
1453
+ orgId,
1454
+ orgName,
1455
+ ...asString(obj.role_name) ?? asString(obj.roleName) !== void 0 ? { roleName: asString(obj.role_name) ?? asString(obj.roleName) } : {},
1456
+ isSuspended: asBoolean(obj.is_suspended) ?? asBoolean(obj.isSuspended) ?? false,
1457
+ canManageSubscriptions: asBoolean(obj.can_manage_subscriptions) ?? asBoolean(obj.canManageSubscriptions) ?? false,
1458
+ resourcePackageFeatureEnabled: asBoolean(obj.resource_package_feature_enabled) ?? asBoolean(obj.resourcePackageFeatureEnabled) ?? false
1459
+ };
1460
+ }
1461
+ function normalizeFeatureAllowed(raw) {
1462
+ if (!raw || typeof raw !== "object") return void 0;
1463
+ const obj = raw;
1464
+ return {
1465
+ quest: asBoolean(obj.quest) ?? false,
1466
+ wiki: asBoolean(obj.wiki) ?? false,
1467
+ codeReview: asBoolean(obj.code_review) ?? asBoolean(obj.codeReview) ?? false
1468
+ };
1469
+ }
1470
+ function normalizePlan(raw) {
1471
+ if (!raw || typeof raw !== "object") return void 0;
1472
+ const obj = raw;
1473
+ const userType = asString(obj.user_type) ?? asString(obj.userType);
1474
+ const planTierName = asString(obj.plan_tier_name) ?? asString(obj.planTierName) ?? asString(obj.plan_name) ?? asString(obj.planName);
1475
+ if (!userType || !planTierName) return void 0;
1476
+ const organization = normalizeOrganization(obj.organization);
1477
+ const isPersonalVersion = asBoolean(obj.is_personal_version) ?? asBoolean(obj.isPersonalVersion) ?? organization === void 0;
1478
+ const startDate = normalizeExpiresAt(obj.start_date ?? obj.startDate);
1479
+ const endDate = normalizeExpiresAt(obj.end_date ?? obj.endDate);
1480
+ const planTier = asString(obj.plan_tier) ?? asString(obj.planTier);
1481
+ const isHighestTier = asBoolean(obj.is_highest_tier) ?? asBoolean(obj.isHighestTier);
1482
+ const isRenewed = asBoolean(obj.is_renewed) ?? asBoolean(obj.isRenewed);
1483
+ const featureAllowed = normalizeFeatureAllowed(obj.feature_allowed ?? obj.featureAllowed);
1484
+ return {
1485
+ userType,
1486
+ planTierName,
1487
+ ...planTier !== void 0 ? { planTier } : {},
1488
+ isPersonalVersion,
1489
+ ...isHighestTier !== void 0 ? { isHighestTier } : {},
1490
+ ...isRenewed !== void 0 ? { isRenewed } : {},
1491
+ ...startDate !== void 0 ? { startDate } : {},
1492
+ ...endDate !== void 0 ? { endDate } : {},
1493
+ ...organization !== void 0 ? { organization } : {},
1494
+ ...featureAllowed !== void 0 ? { featureAllowed } : {},
1495
+ raw
1496
+ };
1497
+ }
1498
+ function normalizeStatus(raw) {
1499
+ if (!raw || typeof raw !== "object") return void 0;
1500
+ const obj = raw;
1501
+ const featureSwitches = obj.featureSwitches ?? obj.feature_switches;
1502
+ const teamSwitches = obj.teamSwitches ?? obj.team_switches;
1503
+ const allowByok = asNumber(featureSwitches?.allow_byok ?? featureSwitches?.allowByok) ?? 0;
1504
+ const teamAllowByok = asNumber(teamSwitches?.allow_byok ?? teamSwitches?.allowByok);
1505
+ const isPrivacyPolicyModifiable = asBoolean(obj.isPrivacyPolicyModifiable ?? obj.is_data_policy_modifiable);
1506
+ return {
1507
+ allowByok,
1508
+ ...teamAllowByok !== void 0 ? { teamAllowByok } : {},
1509
+ ...isPrivacyPolicyModifiable !== void 0 ? { isPrivacyPolicyModifiable } : {},
1510
+ raw
1511
+ };
1512
+ }
1513
+ var QoderUsageReader = class {
1514
+ authService;
1515
+ fetchImpl;
1516
+ ttlMs;
1517
+ timeoutMs;
1518
+ region;
1519
+ logger;
1520
+ cache = /* @__PURE__ */ new Map();
1521
+ flights = new SingleFlight();
1522
+ constructor(options) {
1523
+ this.authService = options.authService;
1524
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
1525
+ this.ttlMs = options.ttlMs ?? defaultUsageTtlMs;
1526
+ this.timeoutMs = options.timeoutMs ?? defaultUsageTimeoutMs;
1527
+ this.region = options.region ?? "global";
1528
+ this.logger = options.logger;
1529
+ }
1530
+ async readAccount(pat, options) {
1531
+ if (!pat || typeof pat !== "string") throw new QoderLlmError("Qoder Personal Access Token is missing or invalid.", "MISSING_CREDENTIAL");
1532
+ const cacheKey = `${this.region}:${opaqueCredentialKey(pat)}`;
1533
+ if (!options?.force) {
1534
+ const cached = this.cache.get(cacheKey);
1535
+ if (cached && cached.expiresAt > Date.now()) return cached.info;
1536
+ }
1537
+ return this.flights.run(cacheKey, options?.signal, (sharedSignal) => this.loadAccount(pat, sharedSignal, cacheKey), () => new QoderLlmError("Qoder account request was aborted.", "ABORTED"));
1538
+ }
1539
+ async loadAccount(pat, signal, cacheKey) {
1540
+ const creds = await this.authService.getCredentials(pat, signal);
1541
+ const profile = {
1542
+ id: creds.userID,
1543
+ name: creds.name || "Qoder User",
1544
+ email: creds.email || ""
1545
+ };
1546
+ const [usage, plan, status] = await Promise.all([
1547
+ retryMetadataRead(signal, () => this.fetchUsage(creds.authToken, signal)),
1548
+ this.safeFetchPlan(creds.authToken, signal),
1549
+ this.safeFetchStatus(creds.authToken, creds.machineID, signal)
1550
+ ]);
1551
+ const accountInfo = {
1552
+ profile,
1553
+ usage,
1554
+ ...plan !== void 0 ? { plan } : {},
1555
+ ...status !== void 0 ? { status } : {},
1556
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1557
+ };
1558
+ this.cache.set(cacheKey, {
1559
+ info: accountInfo,
1560
+ expiresAt: Date.now() + this.ttlMs
1561
+ });
1562
+ return accountInfo;
1563
+ }
1564
+ clear(pat) {
1565
+ if (pat) this.cache.delete(`${this.region}:${opaqueCredentialKey(pat)}`);
1566
+ else this.cache.clear();
1567
+ }
1568
+ async fetchUsage(jobToken, signal) {
1569
+ const data = await openApiJsonRequest(this.fetchImpl, {
1570
+ url: getQoderUsageUrl(this.region),
1571
+ token: jobToken,
1572
+ signal,
1573
+ timeoutMs: this.timeoutMs,
1574
+ logger: this.logger,
1575
+ operation: "Usage",
1576
+ logCategory: "account.usage"
1577
+ });
1578
+ return {
1579
+ userQuota: normalizeQuota(data.userQuota),
1580
+ orgResourcePackage: normalizeQuota(data.orgResourcePackage),
1581
+ totalUsagePercentage: typeof data.totalUsagePercentage === "number" ? data.totalUsagePercentage : void 0,
1582
+ isQuotaExceeded: typeof data.isQuotaExceeded === "boolean" ? data.isQuotaExceeded : false,
1583
+ expiresAt: normalizeExpiresAt(data.expiresAt),
1584
+ raw: data
1585
+ };
1586
+ }
1587
+ async fetchPlan(jobToken, signal) {
1588
+ return normalizePlan(await openApiJsonRequest(this.fetchImpl, {
1589
+ url: getQoderUserPlanUrl(this.region),
1590
+ token: jobToken,
1591
+ signal,
1592
+ timeoutMs: this.timeoutMs,
1593
+ logger: this.logger,
1594
+ operation: "Plan",
1595
+ logCategory: "account.plan"
1596
+ }));
1597
+ }
1598
+ async safeFetchPlan(jobToken, signal) {
1599
+ try {
1600
+ return await this.fetchPlan(jobToken, signal);
1601
+ } catch (error) {
1602
+ if (signal.aborted) throw error;
1603
+ this.logger?.warn?.("[Qoder Plan] Failed to load user plan (degraded)", error instanceof Error ? error.message : error);
1604
+ return;
1605
+ }
1606
+ }
1607
+ async fetchStatus(jobToken, machineId, signal) {
1608
+ return normalizeStatus(await openApiJsonRequest(this.fetchImpl, {
1609
+ url: getQoderUserStatusUrl(this.region),
1610
+ token: jobToken,
1611
+ machineId,
1612
+ signal,
1613
+ timeoutMs: this.timeoutMs,
1614
+ logger: this.logger,
1615
+ operation: "Status",
1616
+ logCategory: "account.status"
1617
+ }));
1618
+ }
1619
+ async safeFetchStatus(jobToken, machineId, signal) {
1620
+ try {
1621
+ return await this.fetchStatus(jobToken, machineId, signal);
1622
+ } catch (error) {
1623
+ if (signal?.aborted) throw error;
1624
+ this.logger?.warn?.("[Qoder Status] Failed to load user status (degraded)", error instanceof Error ? error.message : error);
1625
+ return;
1626
+ }
1627
+ }
1628
+ };
1629
+ //#endregion
1630
+ //#region src/qoder/transport/wire/encoding.ts
1631
+ /**
1632
+ * Qoder WAF Body encoding algorithm.
1633
+ *
1634
+ * Re-arranges standard Base64 chunks and translates characters against
1635
+ * a custom alphabet to satisfy Qoder upstream WAF requirements.
1636
+ *
1637
+ * @module dsh-provider-qoder/qoder/transport/wire/encoding
1638
+ */
1639
+ const qoderCustomAlphabet = "_doRTgHZBKcGVjlvpC,@aFSx#DPuNJme&i*MzLOEn)sUrthbf%Y^w.(kIQyXqWA!";
1640
+ const qoderStdAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1641
+ function qoderEncodeBody(plaintext) {
1642
+ const std = Buffer.isBuffer(plaintext) ? plaintext.toString("base64") : Buffer.from(plaintext).toString("base64");
1643
+ const n = std.length;
1644
+ const a = Math.floor(n / 3);
1645
+ const rearranged = std.slice(n - a) + std.slice(a, n - a) + std.slice(0, a);
1646
+ let out = "";
1647
+ for (let i = 0; i < n; i++) {
1648
+ const c = rearranged[i];
1649
+ if (c === "=") out += "$";
1650
+ else {
1651
+ const idx = qoderStdAlphabet.indexOf(c);
1652
+ if (idx >= 0) out += qoderCustomAlphabet[idx];
1653
+ else out += c;
1654
+ }
1655
+ }
1656
+ return out;
644
1657
  }
645
1658
  //#endregion
646
- //#region src/thinking.ts
1659
+ //#region src/qoder/transport/wire/thinking.ts
647
1660
  const thinkingTags = [{
648
1661
  open: "<thinking>",
649
1662
  close: "</thinking>"
@@ -800,7 +1813,7 @@ var QoderThinkingParser = class {
800
1813
  }
801
1814
  };
802
1815
  //#endregion
803
- //#region src/sse.ts
1816
+ //#region src/qoder/transport/wire/sse.ts
804
1817
  /** Decode Qoder's outer SSE envelope and inner model chunks. */
805
1818
  const doneMarker = "[DONE]";
806
1819
  function malformed(message) {
@@ -866,6 +1879,7 @@ async function* parseQoderSse(stream, options = {}) {
866
1879
  let pendingUsage;
867
1880
  let terminalKind = "stop";
868
1881
  let sawContent = false;
1882
+ const maxBufferChars = options.maxBufferChars ?? 2097152;
869
1883
  const closeTextual = () => {
870
1884
  if (activeTextual === void 0) return [];
871
1885
  const state = activeTextual;
@@ -942,6 +1956,7 @@ async function* parseQoderSse(stream, options = {}) {
942
1956
  } else {
943
1957
  options.onActivity?.();
944
1958
  buffer += decoder.decode(value, { stream: true });
1959
+ if (buffer.length > maxBufferChars) throw malformed("Qoder SSE frame exceeded its size limit.");
945
1960
  }
946
1961
  while (!sawDone) {
947
1962
  const lineEnd = buffer.indexOf("\n");
@@ -1071,531 +2086,217 @@ async function* parseQoderSse(stream, options = {}) {
1071
2086
  reason: {
1072
2087
  kind: "error",
1073
2088
  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
- });
2089
+ message: "Model returned a completed response with no content.",
2090
+ code: EMPTY_RESPONSE_CODE
2091
+ }
2092
+ }
2093
+ };
2094
+ return;
1384
2095
  }
1385
- return models;
1386
- }
1387
- /**
1388
- * Restore Qoder-only metadata after DSH's provider-neutral discovery contract
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
- });
2096
+ yield {
2097
+ type: "finish",
2098
+ reason: { kind: toolCalls.size > 0 ? "tool-calls" : terminalKind }
2099
+ };
1401
2100
  }
1402
- function hasSameQoderDiscoveryMetadata(left, right) {
1403
- return left?.length === right.length && left.every((model, index) => {
1404
- const candidate = right[index];
1405
- return candidate?.id === model.id && discoveredMetadataKeys.every((key) => JSON.stringify(model[key]) === JSON.stringify(candidate[key]));
1406
- });
2101
+ //#endregion
2102
+ //#region src/qoder/transport/chat.ts
2103
+ /** Qoder chat request encoding, timeout lifecycle, and SSE streaming. */
2104
+ function aborted$1(message) {
2105
+ return new QoderLlmError(message, "ABORTED");
1407
2106
  }
1408
- async function fetchQoderModels(credentials, options = {}) {
1409
- const url = getQoderModelListUrl(options.region);
1410
- const fetchImpl = options.fetch ?? globalThis.fetch;
1411
- options.logger?.debug?.("[Qoder Models] Requesting model catalog", { url });
1412
- let response;
2107
+ async function* streamQoderChat(options, model, credentials, messages, dependencies) {
2108
+ const request = await buildQoderRequestBody(options, credentials.userID, messages, model);
2109
+ const encodedBody = qoderEncodeBody(JSON.stringify(request));
2110
+ const encodedBytes = Buffer.from(encodedBody, "utf8");
2111
+ const chatUrl = getQoderChatUrl(dependencies.region);
2112
+ const requestController = new AbortController();
2113
+ let headerTimedOut = false;
2114
+ let idleTimedOut = false;
2115
+ let idleTimer;
2116
+ const startedAt = performance.now();
2117
+ const onCallerAbort = () => requestController.abort(options.signal?.reason);
2118
+ options.signal?.addEventListener("abort", onCallerAbort, { once: true });
2119
+ const headerTimer = setTimeout(() => {
2120
+ headerTimedOut = true;
2121
+ requestController.abort("response header timeout");
2122
+ }, dependencies.responseHeaderTimeoutMs);
2123
+ const resetIdleTimer = () => {
2124
+ if (idleTimer !== void 0) clearTimeout(idleTimer);
2125
+ idleTimer = setTimeout(() => {
2126
+ idleTimedOut = true;
2127
+ requestController.abort("stream idle timeout");
2128
+ }, dependencies.streamIdleTimeoutMs);
2129
+ };
1413
2130
  try {
1414
- response = await fetchImpl(url, {
1415
- method: "GET",
2131
+ const response = await dependencies.fetch(chatUrl, {
2132
+ method: "POST",
1416
2133
  headers: {
1417
- accept: "application/json",
1418
- ...buildAuthHeaders(null, url, credentials)
2134
+ "content-type": "application/json",
2135
+ "accept": "text/event-stream",
2136
+ "cache-control": "no-cache",
2137
+ "accept-encoding": "identity",
2138
+ "x-model-key": options.model || "cmodel",
2139
+ "x-model-source": model?.source || "system",
2140
+ ...attributionHeaders(),
2141
+ ...buildAuthHeaders(encodedBytes, chatUrl, credentials)
1419
2142
  },
1420
- signal: options.signal
2143
+ body: encodedBytes,
2144
+ signal: requestController.signal
2145
+ });
2146
+ clearTimeout(headerTimer);
2147
+ dependencies.logger?.debug?.("[Qoder Stream] Response headers received", {
2148
+ region: dependencies.region,
2149
+ status: response.status,
2150
+ durationMs: Math.round(performance.now() - startedAt),
2151
+ ...qoderRequestId(response.headers) === void 0 ? {} : { requestId: qoderRequestId(response.headers) }
1421
2152
  });
2153
+ resetIdleTimer();
2154
+ if (!response.ok) throw qoderHttpError(`Qoder upstream service returned HTTP ${response.status}.`, response);
2155
+ if (!response.body) throw new QoderLlmError("Qoder response contains no readable body stream.", "EMPTY_RESPONSE");
2156
+ yield* parseQoderSse(response.body, { onActivity: resetIdleTimer });
1422
2157
  } catch (error) {
1423
- if (options.signal?.aborted) throw new QoderLlmError("Qoder model discovery was aborted.", "ABORTED");
1424
- options.logger?.error?.("[Qoder Models] Catalog network request failed", redactLogValue(error));
1425
- throw new QoderLlmError("Qoder model discovery network request failed.", "TRANSPORT", { cause: error });
1426
- }
1427
- options.logger?.debug?.("[Qoder Models] Catalog request completed", {
1428
- url,
1429
- status: response.status
1430
- });
1431
- const text = await response.text();
1432
- if (!response.ok) {
1433
- options.logger?.error?.("[Qoder Models] Catalog request failed", redactLogPayload(text));
1434
- throw qoderHttpError(`Qoder model discovery failed with HTTP status ${response.status}.`, response);
1435
- }
1436
- let payload;
1437
- try {
1438
- payload = JSON.parse(text);
1439
- } catch {
1440
- throw new QoderLlmError("Qoder model discovery returned invalid JSON.", "MALFORMED_RESPONSE");
2158
+ if (options.signal?.aborted) throw aborted$1("Request was aborted.");
2159
+ if (headerTimedOut) {
2160
+ const failure = new QoderLlmError("Qoder model request exceeded its response header timeout.", "TIMEOUT");
2161
+ dependencies.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(failure));
2162
+ throw failure;
2163
+ }
2164
+ if (idleTimedOut) {
2165
+ const failure = new QoderLlmError("Qoder model stream exceeded its idle timeout.", "TIMEOUT");
2166
+ dependencies.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(failure));
2167
+ throw failure;
2168
+ }
2169
+ if (error instanceof QoderLlmError) {
2170
+ dependencies.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(error));
2171
+ throw error;
2172
+ }
2173
+ const failure = new QoderLlmError("Qoder transport request failed.", "TRANSPORT", { cause: error });
2174
+ dependencies.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(failure), { cause: redactLogValue(error) });
2175
+ throw failure;
2176
+ } finally {
2177
+ clearTimeout(headerTimer);
2178
+ if (idleTimer !== void 0) clearTimeout(idleTimer);
2179
+ options.signal?.removeEventListener("abort", onCallerAbort);
2180
+ requestController.abort("request complete");
1441
2181
  }
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
2182
  }
1447
2183
  //#endregion
1448
- //#region src/usage.ts
1449
- const userAgent = "dsh-provider-qoder";
1450
- const defaultUsageTtlMs = 6e4;
1451
- const defaultUsageTimeoutMs = 15e3;
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
- }
2184
+ //#region src/qoder/transport/default-transport.ts
2185
+ const defaultStreamIdleTimeoutMs = 300 * 1e3;
2186
+ function aborted(message) {
2187
+ return new QoderLlmError(message, "ABORTED");
1476
2188
  }
1477
- var QoderUsageReader = class {
1478
- authService;
1479
- fetchImpl;
1480
- ttlMs;
1481
- timeoutMs;
1482
- resolveRegion;
2189
+ var DefaultQoderTransport = class {
1483
2190
  region;
2191
+ resolvePat;
2192
+ fetchImpl;
1484
2193
  logger;
1485
- cache = /* @__PURE__ */ new Map();
2194
+ streamIdleTimeoutMs;
2195
+ responseHeaderTimeoutMs;
2196
+ metadataTimeoutMs;
2197
+ auth;
2198
+ usage;
2199
+ modelFlights = new SingleFlight();
2200
+ attachments;
2201
+ imageUploader;
1486
2202
  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
2203
  this.region = options.region;
2204
+ this.resolvePat = options.resolvePat;
2205
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
1493
2206
  this.logger = options.logger;
2207
+ this.streamIdleTimeoutMs = options.streamIdleTimeoutMs ?? 3e5;
2208
+ this.responseHeaderTimeoutMs = options.responseHeaderTimeoutMs ?? 3e4;
2209
+ this.metadataTimeoutMs = options.metadataTimeoutMs;
2210
+ this.attachments = options.attachments;
2211
+ this.auth = new QoderAuthService({
2212
+ fetch: this.fetchImpl,
2213
+ logger: this.logger,
2214
+ region: this.region,
2215
+ resolveMachineId: options.resolveMachineId
2216
+ });
2217
+ this.usage = new QoderUsageReader({
2218
+ authService: this.auth,
2219
+ fetch: this.fetchImpl,
2220
+ logger: this.logger,
2221
+ region: this.region,
2222
+ timeoutMs: this.metadataTimeoutMs
2223
+ });
2224
+ this.imageUploader = new QoderImageUploader({
2225
+ fetch: this.fetchImpl,
2226
+ logger: this.logger,
2227
+ region: this.region,
2228
+ ...options.imageUploadTimeoutMs === void 0 ? {} : { timeoutMs: options.imageUploadTimeoutMs },
2229
+ ...options.imageUrlCacheTtlMs === void 0 ? {} : { cacheTtlMs: options.imageUrlCacheTtlMs },
2230
+ refreshCredentials: async (signal) => {
2231
+ const pat = await this.requirePat(signal);
2232
+ this.auth.clear(pat);
2233
+ return this.auth.getCredentials(pat, signal);
2234
+ }
2235
+ });
1494
2236
  }
1495
- setRegion(region) {
1496
- this.region = region;
2237
+ stream(options, model) {
2238
+ return this.generate(options, model);
1497
2239
  }
1498
- currentRegion() {
1499
- if (this.resolveRegion) return this.resolveRegion();
1500
- return this.region ?? "global";
2240
+ async discoverModels(signal) {
2241
+ const pat = await this.requirePat(signal);
2242
+ const key = opaqueCredentialKey(pat);
2243
+ return this.modelFlights.run(key, signal, async (sharedSignal) => {
2244
+ const credentials = await this.auth.getCredentials(pat, sharedSignal);
2245
+ return retryMetadataRead(sharedSignal, () => fetchQoderModels(credentials, {
2246
+ fetch: this.fetchImpl,
2247
+ signal: sharedSignal,
2248
+ logger: this.logger,
2249
+ region: this.region,
2250
+ timeoutMs: this.metadataTimeoutMs
2251
+ }));
2252
+ }, () => aborted("Qoder model discovery was aborted."));
1501
2253
  }
1502
- async readAccount(pat, options) {
1503
- if (!pat || typeof pat !== "string") throw new QoderLlmError("Qoder Personal Access Token is missing or invalid.", "MISSING_CREDENTIAL");
1504
- const effectiveRegion = options?.region ?? this.currentRegion();
1505
- const cacheKey = `${effectiveRegion}:${pat}`;
1506
- if (!options?.force) {
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
2254
+ async readAccount(options) {
2255
+ const pat = await this.requirePat(options?.signal);
2256
+ return this.usage.readAccount(pat, {
2257
+ force: options?.force,
2258
+ signal: options?.signal
1523
2259
  });
1524
- return accountInfo;
1525
2260
  }
1526
- clear(pat) {
1527
- if (pat) {
1528
- for (const key of this.cache.keys()) if (key.endsWith(`:${pat}`) || key === pat) this.cache.delete(key);
1529
- } else this.cache.clear();
1530
- }
1531
- async fetchUsage(jobToken, signal, region) {
1532
- const url = getQoderUsageUrl(region ?? this.currentRegion());
1533
- const timeoutSignal = AbortSignal.timeout(this.timeoutMs);
1534
- const requestSignal = signal === void 0 ? timeoutSignal : AbortSignal.any([signal, timeoutSignal]);
1535
- this.logger?.debug?.("[Qoder Usage] Requesting quota usage", { url });
1536
- try {
1537
- const response = await this.fetchImpl(url, {
1538
- method: "GET",
1539
- headers: {
1540
- authorization: `Bearer ${jobToken}`,
1541
- accept: "application/json",
1542
- "user-agent": userAgent,
1543
- "cosy-version": "1.0.1",
1544
- "cosy-clienttype": "5"
1545
- },
1546
- signal: requestSignal
1547
- });
1548
- this.logger?.debug?.("[Qoder Usage] Request completed", {
1549
- status: response.status,
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
- }
2261
+ async requirePat(signal) {
2262
+ if (signal?.aborted) throw aborted("Qoder request was aborted.");
2263
+ const pat = (await this.resolvePat()).trim();
2264
+ if (!pat) throw new QoderLlmError("Qoder Personal Access Token is missing. Configure Qoder in the Qoder settings page.", "MISSING_CREDENTIAL");
2265
+ if (signal?.aborted) throw aborted("Qoder request was aborted.");
2266
+ return pat;
2267
+ }
2268
+ async *generate(options, model) {
2269
+ if (options.signal?.aborted) throw aborted("Request was aborted prior to generation.");
2270
+ validateQoderRequestShape(options, model);
2271
+ const pat = await this.requirePat(options.signal);
2272
+ const credentials = await this.auth.getCredentials(pat, options.signal);
2273
+ const messages = await translateQoderMessages(options, this.attachments, {
2274
+ uploader: this.imageUploader,
2275
+ credentials
2276
+ });
2277
+ yield* streamQoderChat(options, model, await this.auth.getCredentials(pat, options.signal), messages, {
2278
+ fetch: this.fetchImpl,
2279
+ logger: this.logger,
2280
+ region: this.region,
2281
+ responseHeaderTimeoutMs: this.responseHeaderTimeoutMs,
2282
+ streamIdleTimeoutMs: this.streamIdleTimeoutMs
2283
+ });
1579
2284
  }
1580
2285
  };
1581
2286
  //#endregion
1582
- //#region src/index.ts
1583
- const name = "provider-qoder";
1584
- const inject = [
1585
- "llm",
1586
- "credentials",
1587
- "connection"
1588
- ];
1589
- const providerQoder = "qoder-official";
1590
- const settingsNamespace = "provider-qoder";
1591
- const qoderChannel = "/qoder-subscription";
1592
- const fiberDisposed = 4;
1593
- const fiberUnloading = 5;
2287
+ //#region src/qoder/transport/index.ts
2288
+ function createQoderTransport(options) {
2289
+ return new DefaultQoderTransport(options);
2290
+ }
2291
+ //#endregion
2292
+ //#region src/dsh/config.ts
2293
+ /** DSH configuration schema and region-scoped model catalog migration rules. */
1594
2294
  const catalogModel = z.object({
1595
2295
  id: z.string().required(),
1596
2296
  name: z.string().required(),
1597
2297
  description: z.string(),
1598
2298
  contextWindow: z.number().step(1).min(1),
2299
+ maxContextWindow: z.number().step(1).min(1),
1599
2300
  maxTokens: z.number().step(1).min(1),
1600
2301
  source: z.string(),
1601
2302
  isReasoning: z.boolean(),
@@ -1607,14 +2308,17 @@ const catalogModel = z.object({
1607
2308
  })),
1608
2309
  defaultReasoningEffort: z.string(),
1609
2310
  priceFactor: z.number().min(0),
2311
+ supportsImages: z.boolean(),
1610
2312
  contextOptions: z.dict(z.object({
1611
2313
  tokenCount: z.number().step(1).min(1),
1612
2314
  isDefault: z.boolean()
1613
2315
  }))
1614
2316
  });
2317
+ const modelsByRegionSchema = z.dict(z.array(catalogModel));
1615
2318
  const Config = z.object({
1616
2319
  region: z.union(["global", "china"]).default("global"),
1617
- models: z.array(catalogModel).default(defaultModels),
2320
+ modelsByRegion: modelsByRegionSchema.default({}),
2321
+ models: z.array(catalogModel),
1618
2322
  streamIdleTimeoutMs: z.number().step(1).min(1).default(defaultStreamIdleTimeoutMs)
1619
2323
  });
1620
2324
  function resolveModels(models) {
@@ -1628,6 +2332,117 @@ function resolveModels(models) {
1628
2332
  return { ...model };
1629
2333
  });
1630
2334
  }
2335
+ function modelsFor(config, region, legacyModelsRegion = config.region ?? "global") {
2336
+ const scoped = config.modelsByRegion?.[region];
2337
+ if (scoped !== void 0) return resolveModels(scoped);
2338
+ if (legacyModelsRegion === region && config.models !== void 0 && config.models.length > 0) return resolveModels(config.models);
2339
+ return resolveModels(defaultModels);
2340
+ }
2341
+ //#endregion
2342
+ //#region src/dsh/adapter.ts
2343
+ /** Thin DSH adapter over the Qoder transport seam. */
2344
+ function modelInfo(provider, model) {
2345
+ return {
2346
+ provider,
2347
+ id: model.id,
2348
+ name: model.priceFactor === void 0 ? model.name : `${model.name} (${model.priceFactor}x)`,
2349
+ description: model.description,
2350
+ inputModalities: model.supportsImages === true ? ["text", "image"] : ["text"]
2351
+ };
2352
+ }
2353
+ var QoderAdapter = class extends LlmAdapter {
2354
+ resolveTransport;
2355
+ catalogModels;
2356
+ providerId;
2357
+ providerName;
2358
+ constructor(options) {
2359
+ super();
2360
+ this.resolveTransport = options.resolveTransport;
2361
+ this.catalogModels = options.models && options.models.length > 0 ? options.models : defaultModels;
2362
+ this.providerId = options.providerId ?? "qoder-official";
2363
+ this.providerName = options.providerName ?? "Qoder";
2364
+ }
2365
+ providerInfo(provider) {
2366
+ return {
2367
+ id: provider,
2368
+ name: this.providerName
2369
+ };
2370
+ }
2371
+ listModels(provider) {
2372
+ return Promise.resolve(this.catalogModels.map((model) => modelInfo(provider, model)));
2373
+ }
2374
+ replaceModels(models) {
2375
+ this.catalogModels = models;
2376
+ }
2377
+ resolveModel(provider, modelId, signal) {
2378
+ if (signal?.aborted) return Promise.reject(new QoderLlmError("Qoder model resolution was aborted.", "ABORTED"));
2379
+ const configured = this.catalogModels.find((model) => model.id === modelId);
2380
+ if (configured === void 0) return Promise.resolve({
2381
+ provider,
2382
+ id: modelId,
2383
+ name: modelId,
2384
+ inputModalities: ["text"]
2385
+ });
2386
+ return Promise.resolve({
2387
+ ...modelInfo(provider, configured),
2388
+ ...configured.contextWindow === void 0 ? {} : { context: { contextWindow: configured.contextWindow } },
2389
+ ...configured.maxTokens === void 0 ? {} : { defaultMaxTokens: configured.maxTokens },
2390
+ ...configured.reasoningEfforts === void 0 || configured.reasoningEfforts.length === 0 ? {} : { reasoning: {
2391
+ efforts: configured.reasoningEfforts.map((effort) => ({
2392
+ id: ReasoningEffortId(effort.id),
2393
+ name: effort.name,
2394
+ ...effort.description === void 0 ? {} : { description: effort.description }
2395
+ })),
2396
+ ...configured.isReasoning === false || configured.defaultReasoningEffort === void 0 ? {} : { defaultEffort: ReasoningEffortId(configured.defaultReasoningEffort) }
2397
+ } }
2398
+ });
2399
+ }
2400
+ stream(options) {
2401
+ if (options.provider !== this.providerId) throw new QoderLlmError(`Qoder adapter does not own provider "${options.provider}".`, "INVALID_PROVIDER");
2402
+ const model = this.catalogModels.find((candidate) => candidate.id === (options.model || "cmodel"));
2403
+ return this.resolveTransport().stream(options, model);
2404
+ }
2405
+ };
2406
+ //#endregion
2407
+ //#region src/dsh/credential-contract.ts
2408
+ /** Shared DSH host/browser contract for the one managed Qoder credential. */
2409
+ const qoderCredentialRef = "QODER_MANAGED_CREDENTIAL";
2410
+ /** DSH's local provider uses `env` and `*-env` for ambient credential layers. */
2411
+ function isEnvironmentCredentialSource(source) {
2412
+ return source === "env" || source?.endsWith("-env") === true;
2413
+ }
2414
+ //#endregion
2415
+ //#region src/dsh/credential.ts
2416
+ /** DSH host-side resolution of the one managed Qoder credential. */
2417
+ const managedQoderCredentialRef = credentialRef(qoderCredentialRef);
2418
+ /** Resolve only provider-managed storage; ambient environment layers are intentionally unsupported. */
2419
+ async function resolveManagedQoderPat(credentials) {
2420
+ const hit = await credentials.resolve(managedQoderCredentialRef);
2421
+ if (hit === void 0 || isEnvironmentCredentialSource(hit.source)) return "";
2422
+ return hit.value.trim();
2423
+ }
2424
+ //#endregion
2425
+ //#region src/dsh/plugin.ts
2426
+ const name = "provider-qoder";
2427
+ const inject = [
2428
+ "llm",
2429
+ "credentials",
2430
+ "connection",
2431
+ "attachments"
2432
+ ];
2433
+ const providerQoder = "qoder-official";
2434
+ const settingsNamespace = "provider-qoder";
2435
+ const qoderChannel = "/qoder-subscription";
2436
+ const fiberDisposed = 4;
2437
+ const fiberUnloading = 5;
2438
+ function logError(error) {
2439
+ if (!(error instanceof Error)) return { name: "UnknownError" };
2440
+ return {
2441
+ name: error.name,
2442
+ message: error.message,
2443
+ ..."code" in error ? { code: error.code } : {}
2444
+ };
2445
+ }
1631
2446
  function publicError(message) {
1632
2447
  return {
1633
2448
  ok: false,
@@ -1640,81 +2455,114 @@ function publicError(message) {
1640
2455
  }
1641
2456
  function apply(ctx, config = {}) {
1642
2457
  const logger = ctx.logger;
1643
- let current = () => baseConfig;
1644
- const authService = new QoderAuthService({
1645
- logger,
1646
- resolveRegion: () => current().region ?? "global"
1647
- });
1648
- const usageReader = new QoderUsageReader({
1649
- authService,
1650
- logger,
1651
- resolveRegion: () => current().region ?? "global"
1652
- });
2458
+ const initialRegion = config.region ?? "global";
2459
+ const hasConfiguredModels = config.models !== void 0 && config.models.length > 0;
1653
2460
  const baseConfig = {
1654
- region: config.region ?? "global",
1655
- models: resolveModels(config.models),
2461
+ region: initialRegion,
2462
+ modelsByRegion: { ...config.modelsByRegion },
2463
+ ...hasConfiguredModels ? { models: resolveModels(config.models) } : {},
1656
2464
  streamIdleTimeoutMs: config.streamIdleTimeoutMs ?? 3e5
1657
2465
  };
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
2466
+ let current = () => baseConfig;
2467
+ let legacyModelsRegion = initialRegion;
2468
+ let persistDiscoveredModels = async (_region) => {};
2469
+ const discoveredCatalogs = {
2470
+ global: [],
2471
+ china: []
2472
+ };
2473
+ const createTransport = (region, streamIdleTimeoutMs, resolvePat = () => resolveManagedQoderPat(ctx.credentials)) => createQoderTransport({
2474
+ region,
2475
+ resolvePat,
2476
+ logger,
2477
+ streamIdleTimeoutMs,
2478
+ attachments: ctx.attachments
1664
2479
  });
2480
+ const resolveConfig = () => {
2481
+ const value = current();
2482
+ const region = value.region ?? "global";
2483
+ return {
2484
+ region,
2485
+ models: mergeQoderDiscoveryMetadata(modelsFor(value, region, legacyModelsRegion), discoveredCatalogs[region]),
2486
+ streamIdleTimeoutMs: value.streamIdleTimeoutMs ?? 3e5
2487
+ };
2488
+ };
1665
2489
  const initial = resolveConfig();
2490
+ let activeTransport = createTransport(initial.region, initial.streamIdleTimeoutMs);
2491
+ let activeTransportConfig = {
2492
+ region: initial.region,
2493
+ streamIdleTimeoutMs: initial.streamIdleTimeoutMs
2494
+ };
1666
2495
  const adapter = new QoderAdapter({
1667
- resolvePat: () => resolveManagedQoderPat(ctx.credentials),
2496
+ resolveTransport: () => activeTransport,
1668
2497
  models: initial.models,
1669
2498
  providerId: providerQoder,
1670
- providerName: "Qoder",
1671
- authService,
1672
- logger,
1673
- streamIdleTimeoutMs: initial.streamIdleTimeoutMs,
1674
- region: initial.region
2499
+ providerName: "Qoder"
1675
2500
  });
1676
2501
  const registration = ctx.llm.registerAdapter([providerQoder], adapter);
1677
2502
  const refreshAdapter = () => {
1678
2503
  const next = resolveConfig();
1679
- if (next.region !== previousRegion) {
1680
- authService.clear();
1681
- usageReader.clear();
1682
- previousRegion = next.region;
2504
+ if (next.region !== activeTransportConfig.region || next.streamIdleTimeoutMs !== activeTransportConfig.streamIdleTimeoutMs) {
2505
+ activeTransport = createTransport(next.region, next.streamIdleTimeoutMs);
2506
+ activeTransportConfig = {
2507
+ region: next.region,
2508
+ streamIdleTimeoutMs: next.streamIdleTimeoutMs
2509
+ };
1683
2510
  }
1684
- adapter.replaceConfig(next.models, next.streamIdleTimeoutMs, next.region);
2511
+ adapter.replaceModels(next.models);
1685
2512
  registration.replace([providerQoder]);
1686
2513
  };
1687
2514
  ctx.inject(["settings"], (settingsCtx) => {
1688
2515
  const scope = settingsCtx.settings.register(settingsNamespace, Config, {
1689
2516
  base: baseConfig,
1690
2517
  validate: (value) => {
1691
- resolveModels(value.models);
2518
+ for (const region of Object.keys(value.modelsByRegion ?? {})) if (region !== "global" && region !== "china") throw new Error(`provider-qoder: unsupported model catalog region "${region}"`);
2519
+ modelsFor(value, "global", legacyModelsRegion);
2520
+ modelsFor(value, "china", legacyModelsRegion);
1692
2521
  }
1693
2522
  });
2523
+ legacyModelsRegion = scope.get().region ?? initialRegion;
1694
2524
  current = () => scope.get();
2525
+ persistDiscoveredModels = async (region) => {
2526
+ const value = scope.get();
2527
+ const selected = modelsFor(value, region, legacyModelsRegion);
2528
+ const enriched = mergeQoderDiscoveryMetadata(selected, discoveredCatalogs[region]);
2529
+ if (!hasSameQoderDiscoveryMetadata(selected, enriched)) await scope.update({ modelsByRegion: {
2530
+ ...value.modelsByRegion,
2531
+ [region]: enriched
2532
+ } });
2533
+ };
1695
2534
  refreshAdapter();
2535
+ const loaded = scope.get();
2536
+ const loadedRegion = loaded.region ?? "global";
2537
+ if (loaded.models !== void 0 && loaded.models.length > 0 && loaded.modelsByRegion?.[loadedRegion] === void 0) scope.update({ modelsByRegion: {
2538
+ ...loaded.modelsByRegion,
2539
+ [loadedRegion]: resolveModels(loaded.models)
2540
+ } }).catch((error) => logger?.error?.("[Qoder Settings] Failed to migrate model catalog", logError(error)));
1696
2541
  scope.watch(async (next) => {
1697
2542
  if (ctx.fiber.state === fiberUnloading || ctx.fiber.state === fiberDisposed) return;
1698
2543
  refreshAdapter();
1699
- const enriched = mergeQoderDiscoveryMetadata(resolveModels(next.models), discoveredCatalog);
1700
- if (!hasSameQoderDiscoveryMetadata(next.models, enriched)) await scope.update({ models: enriched });
2544
+ const region = next.region ?? "global";
2545
+ const selected = modelsFor(next, region, legacyModelsRegion);
2546
+ const enriched = mergeQoderDiscoveryMetadata(selected, discoveredCatalogs[region]);
2547
+ if (!hasSameQoderDiscoveryMetadata(selected, enriched)) await scope.update({ modelsByRegion: {
2548
+ ...next.modelsByRegion,
2549
+ [region]: enriched
2550
+ } });
1701
2551
  });
1702
2552
  settingsCtx.effect(() => () => {
1703
2553
  if (ctx.fiber.state === fiberUnloading || ctx.fiber.state === fiberDisposed) return;
2554
+ persistDiscoveredModels = async () => {};
1704
2555
  current = () => baseConfig;
1705
2556
  refreshAdapter();
1706
2557
  });
1707
2558
  });
1708
2559
  const discoverModels = async (signal, suppliedPat) => {
1709
- const pat = suppliedPat?.trim() || await resolveManagedQoderPat(ctx.credentials);
1710
- if (!pat) throw new QoderLlmError("Qoder PAT is not configured.", "MISSING_CREDENTIAL");
1711
- const region = current().region ?? "global";
1712
- const models = await fetchQoderModels(await authService.getCredentials(pat, signal, region), {
1713
- signal,
1714
- logger,
1715
- region
1716
- });
1717
- discoveredCatalog = models;
2560
+ const snapshot = resolveConfig();
2561
+ const normalizedPat = suppliedPat?.trim();
2562
+ const models = await (normalizedPat ? createTransport(snapshot.region, snapshot.streamIdleTimeoutMs, () => Promise.resolve(normalizedPat)) : activeTransport).discoverModels(signal);
2563
+ discoveredCatalogs[snapshot.region] = models;
2564
+ refreshAdapter();
2565
+ await persistDiscoveredModels(snapshot.region);
1718
2566
  return models;
1719
2567
  };
1720
2568
  ctx.llm.registerModelDiscovery(settingsNamespace, async (request, signal) => {
@@ -1737,28 +2585,15 @@ function apply(ctx, config = {}) {
1737
2585
  };
1738
2586
  } catch (error) {
1739
2587
  if (signal.aborted || error instanceof QoderLlmError && error.code === "ABORTED") throw error;
1740
- logger?.error?.("[Qoder RPC] Failed to discover models", redactLogValue(error));
2588
+ logger?.error?.("[Qoder RPC] Failed to discover models", logError(error));
1741
2589
  return publicError(error instanceof Error ? error.message : "Failed to discover Qoder models");
1742
2590
  }
1743
2591
  const force = typeof payload === "object" && payload !== null && "force" in payload ? payload.force === true : false;
1744
2592
  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
2593
  try {
1758
- const account = await usageReader.readAccount(pat, {
2594
+ const account = await activeTransport.readAccount({
1759
2595
  force,
1760
- signal,
1761
- region: current().region ?? "global"
2596
+ signal
1762
2597
  });
1763
2598
  logger?.debug?.("[Qoder RPC] Subscriber account resolved");
1764
2599
  return {
@@ -1767,7 +2602,7 @@ function apply(ctx, config = {}) {
1767
2602
  };
1768
2603
  } catch (error) {
1769
2604
  if (signal.aborted || error instanceof QoderLlmError && error.code === "ABORTED") throw error;
1770
- logger?.error?.("[Qoder RPC] Failed to read subscriber account", redactLogValue(error));
2605
+ logger?.error?.("[Qoder RPC] Failed to read subscriber account", logError(error));
1771
2606
  return publicError(error instanceof Error ? error.message : "Failed to load Qoder account");
1772
2607
  }
1773
2608
  };
@@ -1775,4 +2610,4 @@ function apply(ctx, config = {}) {
1775
2610
  }
1776
2611
  }
1777
2612
  //#endregion
1778
- export { Config, QoderAdapter, QoderAuthService, QoderLlmError, QoderUsageReader, apply, buildAuthHeaders, buildQoderRequestBody, computeSigPath, defaultMaxTokens, defaultModels, defaultStreamIdleTimeoutMs, fetchQoderModels, getMachineId, getQoderChatUrl, getQoderExchangeUrl, getQoderModelListUrl, getQoderUsageUrl, getQoderUserInfoUrl, hasSameQoderDiscoveryMetadata, inject, isEnvironmentCredentialSource, managedQoderCredentialRef, mergeQoderDiscoveryMetadata, name, normalizeQoderModels, parseQoderSse, qoderCredentialRef, qoderEncodeBody, qoderGlobalBaseUrl, qoderGlobalOpenApiUrl, qoderHttpError, qoderRegionEndpoints, redactLogPayload, redactLogValue, resolveManagedQoderPat, resolveQoderEndpoints, retryAfterMs, translateTools, validateAndTranslateMessages, validateQoderRequest };
2613
+ export { Config, apply, inject, name };