dsh-provider-qoder 0.1.0 → 0.2.1

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