dsh-provider-qoder 0.5.0 → 0.5.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/client.js +6 -2
- package/lib/index.js +215 -121
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -672,10 +672,13 @@ window.__ModuleLoader__.load({
|
|
|
672
672
|
const followsConfig = this.snapshot.catalog === this.confirmed;
|
|
673
673
|
this.confirmed = models.map(cloneCatalogModel);
|
|
674
674
|
const selected = new Map(this.confirmed.map((model) => [model.id, model]));
|
|
675
|
+
const catalog = new Map(this.snapshot.catalog.map((model) => [model.id, selected.get(model.id) ?? model]));
|
|
676
|
+
for (const model of this.confirmed) catalog.set(model.id, model);
|
|
675
677
|
this.publish({
|
|
676
678
|
...this.snapshot,
|
|
677
679
|
models: this.confirmed,
|
|
678
|
-
catalog: followsConfig ? this.confirmed :
|
|
680
|
+
catalog: followsConfig ? this.confirmed : [...catalog.values()],
|
|
681
|
+
unavailableIds: new Set([...this.snapshot.unavailableIds].filter((id) => !selected.has(id)))
|
|
679
682
|
});
|
|
680
683
|
}
|
|
681
684
|
activate() {
|
|
@@ -2500,7 +2503,8 @@ window.__ModuleLoader__.load({
|
|
|
2500
2503
|
if (response.ok) {
|
|
2501
2504
|
const region = regionOf(readModelSnapshot().value);
|
|
2502
2505
|
const current = await authorizationOperations.read(region);
|
|
2503
|
-
if (current.ok
|
|
2506
|
+
if (!current.ok) return false;
|
|
2507
|
+
if (current.value.method !== "pat") return (await authorizationOperations.select(region, "pat")).ok;
|
|
2504
2508
|
}
|
|
2505
2509
|
return response.ok;
|
|
2506
2510
|
} catch {
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
|
-
import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE, ReasoningEffortId, ToolCallId, attributionHeaders,
|
|
2
|
+
import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE, ReasoningEffortId, ToolCallId, attributionHeaders, isContextWindowExceededError, isQuotaExceededError } from "@deepseek-ai/dsh-llm";
|
|
3
3
|
import crypto, { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir, hostname } from "node:os";
|
|
@@ -1265,6 +1265,9 @@ async function fetchQoderModels(credentials, options = {}) {
|
|
|
1265
1265
|
function unsupported(message) {
|
|
1266
1266
|
return new QoderLlmError(message, "UNSUPPORTED_CONTENT");
|
|
1267
1267
|
}
|
|
1268
|
+
function validateText(block) {
|
|
1269
|
+
if ((block.type === "text" || block.type === "reasoning") && typeof block.text !== "string") throw unsupported("Qoder text and reasoning content must contain a string text value.");
|
|
1270
|
+
}
|
|
1268
1271
|
/** Keep multimodal tool results attached to their call. */
|
|
1269
1272
|
async function translateToolResultContent(blocks, context) {
|
|
1270
1273
|
const supportedBlocks = blocks.map((block) => {
|
|
@@ -1347,7 +1350,49 @@ function consumeToolCallId(message, result, pendingIds, fallbackIndex) {
|
|
|
1347
1350
|
}
|
|
1348
1351
|
return pendingIds.shift() ?? `call_unknown_${fallbackIndex}`;
|
|
1349
1352
|
}
|
|
1353
|
+
/** Validate the entire batch without reading attachments or resolving credentials. */
|
|
1354
|
+
function validateMessageShapes(messages, attachments) {
|
|
1355
|
+
let hasImages = false;
|
|
1356
|
+
const validateImages = (blocks) => {
|
|
1357
|
+
const images = blocks.filter((block) => block.type === "image");
|
|
1358
|
+
hasImages ||= images.length > 0;
|
|
1359
|
+
if (attachments) enforceImageLimits(images, attachments);
|
|
1360
|
+
};
|
|
1361
|
+
for (const message of messages) {
|
|
1362
|
+
if (![
|
|
1363
|
+
"system",
|
|
1364
|
+
"user",
|
|
1365
|
+
"assistant",
|
|
1366
|
+
"tool"
|
|
1367
|
+
].includes(message.role) || !Array.isArray(message.content)) throw unsupported("Qoder received an unsupported message shape.");
|
|
1368
|
+
const results = message.content.filter((block) => block.type === "tool-result");
|
|
1369
|
+
for (const result of results) {
|
|
1370
|
+
if (!Array.isArray(result.content) || result.content.some((block) => block.type !== "text" && block.type !== "image")) throw unsupported("Qoder tool results support text and images only.");
|
|
1371
|
+
for (const block of result.content) validateText(block);
|
|
1372
|
+
validateImages(result.content);
|
|
1373
|
+
}
|
|
1374
|
+
const blocks = message.content.filter((block) => block.type !== "tool-result");
|
|
1375
|
+
for (const block of blocks) {
|
|
1376
|
+
validateText(block);
|
|
1377
|
+
if (block.type === "tool-call") {
|
|
1378
|
+
if (message.role !== "assistant") throw unsupported("Qoder tool-call content is valid only in assistant messages.");
|
|
1379
|
+
if (asCallId(block.id) === void 0 || typeof block.name !== "string" || block.name.trim().length === 0 || typeof block.arguments !== "string") throw unsupported("Qoder tool-call content requires a non-empty string id and name, and string arguments.");
|
|
1380
|
+
}
|
|
1381
|
+
if (message.role === "tool" && block.type !== "text" && block.type !== "image") throw unsupported("Qoder tool results support text and images only.");
|
|
1382
|
+
if (block.type === "image" && message.role !== "user" && message.role !== "tool") throw unsupported("Qoder image content is valid only in user and tool messages.");
|
|
1383
|
+
if (![
|
|
1384
|
+
"text",
|
|
1385
|
+
"image",
|
|
1386
|
+
"reasoning",
|
|
1387
|
+
"tool-call"
|
|
1388
|
+
].includes(block.type)) throw unsupported(`Qoder does not support ${String(block.type)} content.`);
|
|
1389
|
+
}
|
|
1390
|
+
validateImages(blocks);
|
|
1391
|
+
}
|
|
1392
|
+
return hasImages;
|
|
1393
|
+
}
|
|
1350
1394
|
async function validateAndTranslateMessages(messages, systemPrompt, attachments, signal, pipeline) {
|
|
1395
|
+
validateMessageShapes(messages, attachments);
|
|
1351
1396
|
const context = {
|
|
1352
1397
|
attachments,
|
|
1353
1398
|
signal,
|
|
@@ -1401,7 +1446,7 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
|
|
|
1401
1446
|
continue;
|
|
1402
1447
|
}
|
|
1403
1448
|
if (block.type === "image") {
|
|
1404
|
-
if (message.role !== "user"
|
|
1449
|
+
if (message.role !== "user") throw unsupported("Qoder image content is valid only in user and tool messages.");
|
|
1405
1450
|
hasImage = true;
|
|
1406
1451
|
pendingImages.push({
|
|
1407
1452
|
slot: userContent.length,
|
|
@@ -1464,20 +1509,15 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
|
|
|
1464
1509
|
* request reports — so every step of one turn must carry the same run identity
|
|
1465
1510
|
* while the next subscriber prompt opens a new one.
|
|
1466
1511
|
*
|
|
1467
|
-
*
|
|
1468
|
-
*
|
|
1469
|
-
*
|
|
1470
|
-
*
|
|
1471
|
-
*
|
|
1472
|
-
* transport-synthesized markers, and anything an auxiliary call carries continue
|
|
1473
|
-
* the open record.
|
|
1512
|
+
* Hosted calls use a captured host run as their authoritative boundary. The
|
|
1513
|
+
* weakly owned identity survives history projections and standalone-cache eviction.
|
|
1514
|
+
* Standalone calls infer boundaries from the multiset of user-role messages:
|
|
1515
|
+
* unseen subscriber input opens a run; recognized host context continues it.
|
|
1516
|
+
* Auxiliary calls always receive independent identities.
|
|
1474
1517
|
*
|
|
1475
|
-
*
|
|
1476
|
-
*
|
|
1477
|
-
*
|
|
1478
|
-
* the open record instead of opening a new one. Carrying the host's message
|
|
1479
|
-
* identity through translation is the fix, and needs the host to expose it on
|
|
1480
|
-
* the request the adapter receives.
|
|
1518
|
+
* Standalone limit: text identifies a message because wire messages lack host
|
|
1519
|
+
* message IDs. Identical input replacing an earlier occurrence during compaction
|
|
1520
|
+
* can share the open record. Hosted calls use real boundaries instead.
|
|
1481
1521
|
*
|
|
1482
1522
|
* @module dsh-provider-qoder/qoder/transport/wire/turn-identity
|
|
1483
1523
|
*/
|
|
@@ -1566,14 +1606,14 @@ function messageFingerprint(message) {
|
|
|
1566
1606
|
/**
|
|
1567
1607
|
* Resolves the record identity of the turn one request belongs to.
|
|
1568
1608
|
*
|
|
1569
|
-
*
|
|
1570
|
-
*
|
|
1571
|
-
* `maxSessions`.
|
|
1609
|
+
* Standalone session state is bounded by `maxSessions`. Hosted state is weakly
|
|
1610
|
+
* owned by its run handle, which the host and outstanding requests keep alive.
|
|
1572
1611
|
*/
|
|
1573
1612
|
var QoderTurnTracker = class {
|
|
1574
1613
|
maxSessions;
|
|
1575
1614
|
idleSessionTtlMs;
|
|
1576
1615
|
now;
|
|
1616
|
+
hosted = /* @__PURE__ */ new WeakMap();
|
|
1577
1617
|
sessions = /* @__PURE__ */ new Map();
|
|
1578
1618
|
constructor(options = {}) {
|
|
1579
1619
|
this.maxSessions = options.maxSessions ?? defaultMaxSessions;
|
|
@@ -1587,7 +1627,7 @@ var QoderTurnTracker = class {
|
|
|
1587
1627
|
* that requests within one subscriber turn share a single agent run and
|
|
1588
1628
|
* consumption record.
|
|
1589
1629
|
*/
|
|
1590
|
-
resolveTurnIdentity(sessionId, messages, kind = "conversation", requestId = crypto.randomUUID(), modelKey) {
|
|
1630
|
+
resolveTurnIdentity(sessionId, messages, kind = "conversation", requestId = crypto.randomUUID(), modelKey, hostRun) {
|
|
1591
1631
|
const now = this.now();
|
|
1592
1632
|
if (sessionId === void 0 || sessionId.length === 0) return {
|
|
1593
1633
|
requestSetId: `qoder-request-${crypto.randomUUID()}`,
|
|
@@ -1606,6 +1646,7 @@ var QoderTurnTracker = class {
|
|
|
1606
1646
|
businessStage: "start"
|
|
1607
1647
|
};
|
|
1608
1648
|
}
|
|
1649
|
+
if (hostRun !== void 0) return this.resolveHosted(hostRun, sessionId, requestId, modelKey, now);
|
|
1609
1650
|
let state = this.sessions.get(sessionId);
|
|
1610
1651
|
if (state === void 0) {
|
|
1611
1652
|
state = {
|
|
@@ -1673,6 +1714,36 @@ var QoderTurnTracker = class {
|
|
|
1673
1714
|
businessStage
|
|
1674
1715
|
};
|
|
1675
1716
|
}
|
|
1717
|
+
/** Host boundaries are authoritative; message projections cannot open a new run. */
|
|
1718
|
+
resolveHosted(run, scope, requestId, modelKey, now) {
|
|
1719
|
+
let scopes = this.hosted.get(run);
|
|
1720
|
+
if (scopes === void 0) {
|
|
1721
|
+
scopes = /* @__PURE__ */ new Map();
|
|
1722
|
+
this.hosted.set(run, scopes);
|
|
1723
|
+
}
|
|
1724
|
+
const previous = scopes.get(scope);
|
|
1725
|
+
const modelChanged = previous?.modelKey !== void 0 && modelKey !== void 0 && previous.modelKey !== modelKey;
|
|
1726
|
+
if (previous !== void 0 && !modelChanged) {
|
|
1727
|
+
if (modelKey !== void 0) previous.modelKey = modelKey;
|
|
1728
|
+
return {
|
|
1729
|
+
...previous.identity,
|
|
1730
|
+
chatRecordId: requestId,
|
|
1731
|
+
businessStage: "processing"
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
const identity = {
|
|
1735
|
+
requestSetId: `qoder-request-${crypto.randomUUID()}`,
|
|
1736
|
+
chatRecordId: requestId,
|
|
1737
|
+
businessId: crypto.randomUUID(),
|
|
1738
|
+
beginAt: now,
|
|
1739
|
+
businessStage: "start"
|
|
1740
|
+
};
|
|
1741
|
+
scopes.set(scope, {
|
|
1742
|
+
modelKey,
|
|
1743
|
+
identity
|
|
1744
|
+
});
|
|
1745
|
+
return identity;
|
|
1746
|
+
}
|
|
1676
1747
|
/**
|
|
1677
1748
|
* Record id shared by every step of `sessionId`'s current turn.
|
|
1678
1749
|
*
|
|
@@ -1697,13 +1768,14 @@ var QoderTurnTracker = class {
|
|
|
1697
1768
|
beginAt: identity.beginAt
|
|
1698
1769
|
};
|
|
1699
1770
|
}
|
|
1700
|
-
/** Forget one session
|
|
1771
|
+
/** Forget one standalone session. Hosted state follows its run handle lifetime. */
|
|
1701
1772
|
forget(sessionId) {
|
|
1702
1773
|
this.sessions.delete(sessionId);
|
|
1703
1774
|
}
|
|
1704
1775
|
/** Forget every session. */
|
|
1705
1776
|
clear() {
|
|
1706
1777
|
this.sessions.clear();
|
|
1778
|
+
this.hosted = /* @__PURE__ */ new WeakMap();
|
|
1707
1779
|
}
|
|
1708
1780
|
claimSession(sessionId, state) {
|
|
1709
1781
|
this.sessions.set(sessionId, state);
|
|
@@ -1735,7 +1807,6 @@ var QoderTurnTracker = class {
|
|
|
1735
1807
|
};
|
|
1736
1808
|
//#endregion
|
|
1737
1809
|
//#region src/qoder/transport/wire/prepare.ts
|
|
1738
|
-
/** Shared model request preparation, before selecting a wire protocol. */
|
|
1739
1810
|
function stableHash(prefix, ...inputs) {
|
|
1740
1811
|
const hash = crypto.createHash("sha256");
|
|
1741
1812
|
hash.update(prefix);
|
|
@@ -1750,17 +1821,18 @@ const qoderTurnTracker = new QoderTurnTracker();
|
|
|
1750
1821
|
function callKind(options) {
|
|
1751
1822
|
return options.purpose === void 0 ? "conversation" : "auxiliary";
|
|
1752
1823
|
}
|
|
1753
|
-
function validateQoderRequestShape(options, model) {
|
|
1824
|
+
function validateQoderRequestShape(options, model, attachments) {
|
|
1825
|
+
const hasImages = validateMessageShapes(options.messages, attachments);
|
|
1754
1826
|
if (options.reasoningEffort !== void 0) {
|
|
1755
1827
|
const effort = String(options.reasoningEffort);
|
|
1756
1828
|
if (!model?.reasoningEfforts?.some((candidate) => candidate.id === effort)) throw new QoderLlmError(`Qoder model "${options.model}" does not advertise reasoning effort "${effort}".`, "UNSUPPORTED_REASONING_EFFORT");
|
|
1757
1829
|
}
|
|
1758
|
-
if (
|
|
1830
|
+
if (hasImages && model?.supportsImages !== true) throw new QoderLlmError(`Qoder model "${options.model}" does not advertise image input.`, "UNSUPPORTED_CONTENT");
|
|
1759
1831
|
}
|
|
1760
1832
|
function translateQoderMessages(options, attachments, pipeline) {
|
|
1761
1833
|
return validateAndTranslateMessages(options.messages, options.system, attachments, options.signal, pipeline);
|
|
1762
1834
|
}
|
|
1763
|
-
async function prepareQoderRequest(options, userId, translatedMessages, model, attachments, tracker = qoderTurnTracker, identityScope = "") {
|
|
1835
|
+
async function prepareQoderRequest(options, userId, translatedMessages, model, attachments, tracker = qoderTurnTracker, identityScope = "", hostRun) {
|
|
1764
1836
|
if (!userId) throw new QoderLlmError("Qoder request identity is missing.", "AUTH");
|
|
1765
1837
|
if (translatedMessages === void 0) validateQoderRequestShape(options, model);
|
|
1766
1838
|
const messages = translatedMessages ?? await translateQoderMessages(options, attachments);
|
|
@@ -1788,7 +1860,7 @@ async function prepareQoderRequest(options, userId, translatedMessages, model, a
|
|
|
1788
1860
|
identityScope,
|
|
1789
1861
|
userId,
|
|
1790
1862
|
sessionIdParam
|
|
1791
|
-
]), messages, callKind(options), requestId, modelKey);
|
|
1863
|
+
]), messages, callKind(options), requestId, modelKey, hostRun);
|
|
1792
1864
|
return {
|
|
1793
1865
|
requestId,
|
|
1794
1866
|
requestSetId: identity.requestSetId,
|
|
@@ -2335,7 +2407,19 @@ var QoderUsageReader = class {
|
|
|
2335
2407
|
const cached = this.cache.get(cacheKey);
|
|
2336
2408
|
if (cached && cached.expiresAt > Date.now()) return cached.info;
|
|
2337
2409
|
}
|
|
2338
|
-
return this.flights.run(cacheKey, options?.signal,
|
|
2410
|
+
return this.flights.run(cacheKey, options?.signal, async (sharedSignal) => {
|
|
2411
|
+
let credentials;
|
|
2412
|
+
try {
|
|
2413
|
+
return await this.loadAccount(async (signal) => {
|
|
2414
|
+
credentials = await resolve(signal);
|
|
2415
|
+
return credentials;
|
|
2416
|
+
}, sharedSignal, cacheKey);
|
|
2417
|
+
} catch (error) {
|
|
2418
|
+
if (!(error instanceof QoderLlmError) || error.code !== "AUTH" || !credentials || !options?.refresh) throw error;
|
|
2419
|
+
const refreshed = await options.refresh(credentials, sharedSignal);
|
|
2420
|
+
return this.loadAccount(async () => refreshed, sharedSignal, cacheKey);
|
|
2421
|
+
}
|
|
2422
|
+
}, () => new QoderLlmError("Qoder account request was aborted.", "ABORTED"));
|
|
2339
2423
|
}
|
|
2340
2424
|
async loadAccount(resolve, signal, cacheKey) {
|
|
2341
2425
|
const creds = await resolve(signal);
|
|
@@ -3192,8 +3276,8 @@ var ModelQueue = class {
|
|
|
3192
3276
|
};
|
|
3193
3277
|
//#endregion
|
|
3194
3278
|
//#region src/qoder/transport/wire/serialize.ts
|
|
3195
|
-
async function buildQoderRequestBody(options, userId, translatedMessages, model, attachments, tracker, identityScope = "") {
|
|
3196
|
-
const prepared = await prepareQoderRequest(options, userId, translatedMessages, model, attachments, tracker, identityScope);
|
|
3279
|
+
async function buildQoderRequestBody(options, userId, translatedMessages, model, attachments, tracker, identityScope = "", hostRun) {
|
|
3280
|
+
const prepared = await prepareQoderRequest(options, userId, translatedMessages, model, attachments, tracker, identityScope, hostRun);
|
|
3197
3281
|
return {
|
|
3198
3282
|
request_id: prepared.requestId,
|
|
3199
3283
|
request_set_id: prepared.requestSetId,
|
|
@@ -3391,14 +3475,18 @@ async function* parseQoderChunks(stream, decodeFrame, options = {}, behavior = {
|
|
|
3391
3475
|
} else {
|
|
3392
3476
|
options.onActivity?.();
|
|
3393
3477
|
buffer += decoder.decode(value, { stream: true });
|
|
3394
|
-
if (buffer.length > maxBufferChars) throw malformed$1("Qoder SSE frame exceeded its size limit.");
|
|
3395
3478
|
}
|
|
3396
3479
|
while (!sawDone) {
|
|
3397
3480
|
const lineEnd = buffer.indexOf("\n");
|
|
3398
|
-
if (lineEnd === -1)
|
|
3481
|
+
if (lineEnd === -1) {
|
|
3482
|
+
const pending = buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer;
|
|
3483
|
+
if ((pending.startsWith("data:") ? eventDataChars + pending.slice(pending.startsWith("data: ") ? 6 : 5).length + 1 : pending.length) > maxBufferChars) throw malformed$1("Qoder SSE frame exceeded its size limit.");
|
|
3484
|
+
break;
|
|
3485
|
+
}
|
|
3399
3486
|
let line = buffer.substring(0, lineEnd);
|
|
3400
3487
|
buffer = buffer.substring(lineEnd + 1);
|
|
3401
3488
|
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
3489
|
+
if (!line.startsWith("data:") && line.length > maxBufferChars) throw malformed$1("Qoder SSE frame exceeded its size limit.");
|
|
3402
3490
|
if (line.startsWith("event:")) {
|
|
3403
3491
|
eventType = line.substring(6).trim();
|
|
3404
3492
|
continue;
|
|
@@ -3673,13 +3761,11 @@ function parseQoderSse(stream, options = {}) {
|
|
|
3673
3761
|
}
|
|
3674
3762
|
//#endregion
|
|
3675
3763
|
//#region src/qoder/transport/chat.ts
|
|
3676
|
-
/** Qoder chat request encoding, queue recovery, timeout lifecycle, and SSE streaming. */
|
|
3677
3764
|
function aborted$1(message) {
|
|
3678
3765
|
return new QoderLlmError(message, "ABORTED");
|
|
3679
3766
|
}
|
|
3680
3767
|
async function* streamQoderChat(options, model, credentials, messages, dependencies) {
|
|
3681
|
-
const
|
|
3682
|
-
const request = await buildQoderRequestBody(options, credentials.userID, messages, model, void 0, void 0, identityScope);
|
|
3768
|
+
const request = await buildQoderRequestBody(options, credentials.userID, messages, model, void 0, void 0, dependencies.region, dependencies.hostRun);
|
|
3683
3769
|
request.session_type = dependencies.region === "china" ? "qoderclicn" : "qodercli";
|
|
3684
3770
|
const startedAt = performance.now();
|
|
3685
3771
|
let activeCredentials = credentials;
|
|
@@ -4113,7 +4199,6 @@ function parseModelServerSse(stream, options = {}) {
|
|
|
4113
4199
|
}
|
|
4114
4200
|
//#endregion
|
|
4115
4201
|
//#region src/qoder/transport/model-server.ts
|
|
4116
|
-
/** Bearer-authenticated model-server HTTP SSE transport. */
|
|
4117
4202
|
/** Convert the shared Qoder turn into the model-server's OpenAI-shaped request. */
|
|
4118
4203
|
function modelServerBody(request) {
|
|
4119
4204
|
return {
|
|
@@ -4138,8 +4223,7 @@ function modelServerBody(request) {
|
|
|
4138
4223
|
};
|
|
4139
4224
|
}
|
|
4140
4225
|
async function* streamModelServerChat(options, model, credentials, messages, dependencies) {
|
|
4141
|
-
const
|
|
4142
|
-
const request = await prepareQoderRequest(options, credentials.userID, messages, model, void 0, void 0, identityScope);
|
|
4226
|
+
const request = await prepareQoderRequest(options, credentials.userID, messages, model, void 0, void 0, dependencies.region, dependencies.hostRun);
|
|
4143
4227
|
const body = JSON.stringify(modelServerBody(request));
|
|
4144
4228
|
const startedAt = performance.now();
|
|
4145
4229
|
dependencies.logger?.debug?.("[Qoder Stream] Request context", {
|
|
@@ -4418,21 +4502,10 @@ var DefaultQoderTransport = class {
|
|
|
4418
4502
|
}
|
|
4419
4503
|
async readAccount(options) {
|
|
4420
4504
|
const lease = await this.lease(options?.signal);
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
return used;
|
|
4425
|
-
}, {
|
|
4426
|
-
force,
|
|
4427
|
-
signal: options?.signal
|
|
4505
|
+
return this.usage.readWithCredentials(lease.key, (signal) => lease.resolve(signal), {
|
|
4506
|
+
...options,
|
|
4507
|
+
refresh: (rejected, signal) => lease.resolve(signal, rejected.authToken)
|
|
4428
4508
|
});
|
|
4429
|
-
try {
|
|
4430
|
-
return await read();
|
|
4431
|
-
} catch (error) {
|
|
4432
|
-
if (!(error instanceof QoderLlmError) || error.code !== "AUTH" || !used) throw error;
|
|
4433
|
-
await lease.resolve(options?.signal, used.authToken);
|
|
4434
|
-
return read(true);
|
|
4435
|
-
}
|
|
4436
4509
|
}
|
|
4437
4510
|
async searchWeb(request, signal) {
|
|
4438
4511
|
const lease = await this.lease(signal);
|
|
@@ -4470,7 +4543,7 @@ var DefaultQoderTransport = class {
|
|
|
4470
4543
|
async *generate(options, model) {
|
|
4471
4544
|
if (options.signal?.aborted) throw aborted("Request was aborted prior to generation.");
|
|
4472
4545
|
const queueOwner = options.sessionId === void 0 ? void 0 : this.captureQueueLeaseOwner?.(String(options.sessionId));
|
|
4473
|
-
validateQoderRequestShape(options, model);
|
|
4546
|
+
validateQoderRequestShape(options, model, this.attachments);
|
|
4474
4547
|
const lease = await this.lease(options.signal);
|
|
4475
4548
|
const credentials = await lease.resolve(options.signal);
|
|
4476
4549
|
this.credentialLeases.set(credentials, lease);
|
|
@@ -4489,7 +4562,7 @@ var DefaultQoderTransport = class {
|
|
|
4489
4562
|
streamIdleTimeoutMs: this.streamIdleTimeoutMs,
|
|
4490
4563
|
modelQueueMaxWaitMs: this.modelQueueMaxWaitMs,
|
|
4491
4564
|
onQueueUpdate: this.onQueueUpdate,
|
|
4492
|
-
|
|
4565
|
+
hostRun: queueOwner?.run,
|
|
4493
4566
|
onQueueLease: queueOwner?.retain,
|
|
4494
4567
|
resolveCredentials: async (refresh, signal) => {
|
|
4495
4568
|
const current = await lease.resolve(signal);
|
|
@@ -4592,7 +4665,7 @@ var QoderQueueLeases = class {
|
|
|
4592
4665
|
this.end(sessionId);
|
|
4593
4666
|
this.runs.set(sessionId, {
|
|
4594
4667
|
turn,
|
|
4595
|
-
|
|
4668
|
+
run: Object.freeze({ id: randomUUID() }),
|
|
4596
4669
|
leases: /* @__PURE__ */ new Map()
|
|
4597
4670
|
});
|
|
4598
4671
|
}
|
|
@@ -4607,7 +4680,7 @@ var QoderQueueLeases = class {
|
|
|
4607
4680
|
const owner = this.runs.get(sessionId);
|
|
4608
4681
|
if (!owner || this.disposed) return void 0;
|
|
4609
4682
|
return {
|
|
4610
|
-
|
|
4683
|
+
run: owner.run,
|
|
4611
4684
|
retain: (lease) => lease.sessionId === sessionId && this.runs.get(sessionId) === owner && this.retain(lease)
|
|
4612
4685
|
};
|
|
4613
4686
|
};
|
|
@@ -4648,6 +4721,47 @@ var QoderQueueStatusStore = class {
|
|
|
4648
4721
|
}
|
|
4649
4722
|
};
|
|
4650
4723
|
//#endregion
|
|
4724
|
+
//#region src/dsh/transport-runtime.ts
|
|
4725
|
+
function transportConfig(value) {
|
|
4726
|
+
return {
|
|
4727
|
+
region: value.region ?? "global",
|
|
4728
|
+
streamIdleTimeoutMs: value.streamIdleTimeoutMs ?? 3e5,
|
|
4729
|
+
responseHeaderTimeoutMs: value.responseHeaderTimeoutMs ?? 6e4,
|
|
4730
|
+
modelQueueMaxWaitMs: value.modelQueueMaxWaitMs ?? 36e5,
|
|
4731
|
+
preserveThinking: value.preserveThinking ?? true,
|
|
4732
|
+
requestCompression: value.requestCompression ?? false,
|
|
4733
|
+
modelTransport: value.modelTransport ?? "legacy"
|
|
4734
|
+
};
|
|
4735
|
+
}
|
|
4736
|
+
var QoderTransportRuntime = class {
|
|
4737
|
+
snapshot;
|
|
4738
|
+
constructor(value, factory) {
|
|
4739
|
+
this.create = factory;
|
|
4740
|
+
const config = Object.freeze(transportConfig(value));
|
|
4741
|
+
this.snapshot = {
|
|
4742
|
+
config,
|
|
4743
|
+
transport: this.create(config)
|
|
4744
|
+
};
|
|
4745
|
+
}
|
|
4746
|
+
create;
|
|
4747
|
+
get config() {
|
|
4748
|
+
return this.snapshot.config;
|
|
4749
|
+
}
|
|
4750
|
+
get transport() {
|
|
4751
|
+
return this.snapshot.transport;
|
|
4752
|
+
}
|
|
4753
|
+
update(value, force = false) {
|
|
4754
|
+
const config = Object.freeze(transportConfig(value));
|
|
4755
|
+
if (!force && Object.keys(config).every((key) => config[key] === this.config[key])) return false;
|
|
4756
|
+
const transport = this.create(config);
|
|
4757
|
+
this.snapshot = {
|
|
4758
|
+
config,
|
|
4759
|
+
transport
|
|
4760
|
+
};
|
|
4761
|
+
return true;
|
|
4762
|
+
}
|
|
4763
|
+
};
|
|
4764
|
+
//#endregion
|
|
4651
4765
|
//#region src/dsh/catalog-lifecycle.ts
|
|
4652
4766
|
/** Region-scoped Qoder model discovery, effective catalogs, and advisory persistence. */
|
|
4653
4767
|
var QoderCatalogLifecycle = class {
|
|
@@ -4722,10 +4836,11 @@ var QoderCatalogLifecycle = class {
|
|
|
4722
4836
|
return;
|
|
4723
4837
|
}
|
|
4724
4838
|
}
|
|
4725
|
-
/**
|
|
4839
|
+
/** Active discovery supersedes older work; a foreign transport is an isolated preview. */
|
|
4726
4840
|
discover(signal, requestTransport) {
|
|
4727
4841
|
if (this.disposed || signal?.aborted) return Promise.reject(new QoderLlmError("Qoder model discovery was aborted.", "ABORTED"));
|
|
4728
4842
|
this.observeTransport();
|
|
4843
|
+
if (requestTransport && requestTransport !== this.activeTransport) return requestTransport.discoverModels(signal);
|
|
4729
4844
|
const region = this.options.region();
|
|
4730
4845
|
const state = this.catalogs[region];
|
|
4731
4846
|
const transport = this.activeTransport;
|
|
@@ -4863,6 +4978,9 @@ var QoderAdapter = class extends LlmAdapter {
|
|
|
4863
4978
|
});
|
|
4864
4979
|
} catch {}
|
|
4865
4980
|
}
|
|
4981
|
+
forgetSession(sessionId) {
|
|
4982
|
+
for (const key of this.sessionTiers.keys()) if (JSON.parse(key)[1] === sessionId) this.sessionTiers.delete(key);
|
|
4983
|
+
}
|
|
4866
4984
|
subscribeSessionTiers(listener) {
|
|
4867
4985
|
this.tierListeners.add(listener);
|
|
4868
4986
|
return () => {
|
|
@@ -5119,12 +5237,21 @@ function registerQoderRpc(ctx, handler, subscribeTiers) {
|
|
|
5119
5237
|
const registry = ctx.connection?.fetch;
|
|
5120
5238
|
if (typeof registry?.register !== "function") throw new Error("provider-qoder: connection exposes no exact Fetch route registry");
|
|
5121
5239
|
const streams = /* @__PURE__ */ new Set();
|
|
5122
|
-
const disposers =
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5240
|
+
const disposers = [];
|
|
5241
|
+
try {
|
|
5242
|
+
for (const endpoint of qoderRpcEndpoints) disposers.push(registry.register({
|
|
5243
|
+
path: qoderRpcPath(endpoint),
|
|
5244
|
+
methods: ["POST"],
|
|
5245
|
+
requestBody: "buffered",
|
|
5246
|
+
fetch: (request) => endpoint === "sessionTierEvents" && subscribeTiers ? Promise.resolve(sessionTierEvents(request, subscribeTiers, streams)) : handleQoderRpcRequest(endpoint, handler, request)
|
|
5247
|
+
}));
|
|
5248
|
+
} catch (error) {
|
|
5249
|
+
for (const close of streams) close();
|
|
5250
|
+
for (const dispose of disposers.reverse()) try {
|
|
5251
|
+
Promise.resolve(dispose()).catch(() => {});
|
|
5252
|
+
} catch {}
|
|
5253
|
+
throw error;
|
|
5254
|
+
}
|
|
5128
5255
|
return () => {
|
|
5129
5256
|
for (const close of streams) close();
|
|
5130
5257
|
for (const dispose of disposers) dispose();
|
|
@@ -5556,59 +5683,31 @@ function apply(ctx, input = {}) {
|
|
|
5556
5683
|
const logger = ctx.logger;
|
|
5557
5684
|
const initialRegion = config.region ?? "global";
|
|
5558
5685
|
const baseConfig = {
|
|
5559
|
-
|
|
5686
|
+
...transportConfig(config),
|
|
5560
5687
|
modelsByRegion: { ...config.modelsByRegion },
|
|
5561
|
-
|
|
5562
|
-
responseHeaderTimeoutMs: config.responseHeaderTimeoutMs ?? 6e4,
|
|
5563
|
-
modelQueueMaxWaitMs: config.modelQueueMaxWaitMs ?? 36e5,
|
|
5564
|
-
preserveThinking: config.preserveThinking ?? true,
|
|
5565
|
-
webSearchMode: config.webSearchMode ?? "auto",
|
|
5566
|
-
requestCompression: config.requestCompression ?? false,
|
|
5567
|
-
modelTransport: config.modelTransport ?? "legacy"
|
|
5688
|
+
webSearchMode: config.webSearchMode ?? "auto"
|
|
5568
5689
|
};
|
|
5569
5690
|
let current = () => baseConfig;
|
|
5570
5691
|
let authorization;
|
|
5571
|
-
const createTransport = (
|
|
5572
|
-
|
|
5692
|
+
const createTransport = (value, resolvePat) => createQoderTransport({
|
|
5693
|
+
...value,
|
|
5573
5694
|
resolvePat: resolvePat ?? (() => resolveManagedQoderPat(ctx.credentials)),
|
|
5574
|
-
resolveCredential: resolvePat ? void 0 : (signal) => authorization.lease(region, signal),
|
|
5575
|
-
requestCompression: current().requestCompression ?? false,
|
|
5576
|
-
modelTransport: current().modelTransport ?? "legacy",
|
|
5695
|
+
resolveCredential: resolvePat ? void 0 : (signal) => authorization.lease(value.region, signal),
|
|
5577
5696
|
logger,
|
|
5578
|
-
streamIdleTimeoutMs,
|
|
5579
|
-
responseHeaderTimeoutMs,
|
|
5580
5697
|
attachments: ctx.attachments,
|
|
5581
|
-
preserveThinking,
|
|
5582
|
-
modelQueueMaxWaitMs: current().modelQueueMaxWaitMs ?? 36e5,
|
|
5583
5698
|
onQueueUpdate: queueStatus.update,
|
|
5584
5699
|
captureQueueLeaseOwner: leases.capture
|
|
5585
5700
|
});
|
|
5586
5701
|
const resolveConfig = () => {
|
|
5587
5702
|
const value = current();
|
|
5588
|
-
const
|
|
5703
|
+
const transport = transportConfig(value);
|
|
5589
5704
|
return {
|
|
5590
|
-
|
|
5591
|
-
models: modelsFor(value, region),
|
|
5592
|
-
|
|
5593
|
-
responseHeaderTimeoutMs: value.responseHeaderTimeoutMs ?? 6e4,
|
|
5594
|
-
modelQueueMaxWaitMs: value.modelQueueMaxWaitMs ?? 36e5,
|
|
5595
|
-
preserveThinking: value.preserveThinking ?? true,
|
|
5596
|
-
webSearchMode: value.webSearchMode ?? "auto",
|
|
5597
|
-
requestCompression: value.requestCompression ?? false,
|
|
5598
|
-
modelTransport: value.modelTransport ?? "legacy"
|
|
5705
|
+
...transport,
|
|
5706
|
+
models: modelsFor(value, transport.region),
|
|
5707
|
+
webSearchMode: value.webSearchMode ?? "auto"
|
|
5599
5708
|
};
|
|
5600
5709
|
};
|
|
5601
|
-
const
|
|
5602
|
-
let activeTransport = createTransport(initial.region, initial.streamIdleTimeoutMs, initial.responseHeaderTimeoutMs, initial.preserveThinking);
|
|
5603
|
-
let activeTransportConfig = {
|
|
5604
|
-
region: initial.region,
|
|
5605
|
-
streamIdleTimeoutMs: initial.streamIdleTimeoutMs,
|
|
5606
|
-
responseHeaderTimeoutMs: initial.responseHeaderTimeoutMs,
|
|
5607
|
-
modelQueueMaxWaitMs: initial.modelQueueMaxWaitMs,
|
|
5608
|
-
preserveThinking: initial.preserveThinking,
|
|
5609
|
-
requestCompression: initial.requestCompression,
|
|
5610
|
-
modelTransport: initial.modelTransport
|
|
5611
|
-
};
|
|
5710
|
+
const runtime = new QoderTransportRuntime(current(), createTransport);
|
|
5612
5711
|
const authorizationTransports = /* @__PURE__ */ new Map();
|
|
5613
5712
|
authorization = new QoderBrowserAuthorizationManager({
|
|
5614
5713
|
credentials: ctx.credentials,
|
|
@@ -5630,32 +5729,34 @@ function apply(ctx, input = {}) {
|
|
|
5630
5729
|
});
|
|
5631
5730
|
ctx.effect(() => () => authorization.dispose(), "provider-qoder: browser authorization lifecycle");
|
|
5632
5731
|
const catalog = new QoderCatalogLifecycle({
|
|
5633
|
-
resolveTransport: () =>
|
|
5634
|
-
region: () =>
|
|
5732
|
+
resolveTransport: () => runtime.transport,
|
|
5733
|
+
region: () => runtime.config.region,
|
|
5635
5734
|
configuredModels: (region) => modelsFor(current(), region),
|
|
5636
5735
|
onDiscovered: () => registration.replace([providerQoder]),
|
|
5637
5736
|
onPersistenceError: (error) => logger?.error?.("[Qoder Settings] Failed to synchronize model catalog", logError(error))
|
|
5638
5737
|
});
|
|
5639
5738
|
ctx.effect(() => () => catalog.dispose(), "provider-qoder: catalog lifecycle");
|
|
5640
5739
|
const adapter = new QoderAdapter({
|
|
5641
|
-
resolveTransport: () =>
|
|
5740
|
+
resolveTransport: () => runtime.transport,
|
|
5642
5741
|
catalog,
|
|
5643
|
-
region: () =>
|
|
5742
|
+
region: () => runtime.config.region,
|
|
5644
5743
|
providerId: providerQoder,
|
|
5645
5744
|
providerName: "Qoder",
|
|
5646
5745
|
sessions: ctx.get("sessions"),
|
|
5647
5746
|
agents: ctx.get("agents")
|
|
5648
5747
|
});
|
|
5748
|
+
ctx.on("session/disposed", (session) => adapter.forgetSession(session.id));
|
|
5649
5749
|
const registration = ctx.llm.registerAdapter([providerQoder], adapter);
|
|
5650
5750
|
const refreshAuthentication = () => {
|
|
5651
5751
|
const value = resolveConfig();
|
|
5652
|
-
|
|
5752
|
+
runtime.update(value, true);
|
|
5753
|
+
authorization.activateRegion(runtime.config.region);
|
|
5653
5754
|
catalog.clearDiscovered();
|
|
5654
5755
|
catalog.observeTransport();
|
|
5655
5756
|
registration.replace([providerQoder]);
|
|
5656
5757
|
};
|
|
5657
5758
|
ctx.on("credentials/record-updated", (key) => {
|
|
5658
|
-
if (key === browserAuthorizationKey(
|
|
5759
|
+
if (key === browserAuthorizationKey(runtime.config.region)) {
|
|
5659
5760
|
refreshAuthentication();
|
|
5660
5761
|
catalog.refresh();
|
|
5661
5762
|
}
|
|
@@ -5665,19 +5766,7 @@ function apply(ctx, input = {}) {
|
|
|
5665
5766
|
});
|
|
5666
5767
|
const refreshAdapter = () => {
|
|
5667
5768
|
const next = resolveConfig();
|
|
5668
|
-
if (
|
|
5669
|
-
authorization.activateRegion(next.region);
|
|
5670
|
-
activeTransport = createTransport(next.region, next.streamIdleTimeoutMs, next.responseHeaderTimeoutMs, next.preserveThinking);
|
|
5671
|
-
activeTransportConfig = {
|
|
5672
|
-
region: next.region,
|
|
5673
|
-
streamIdleTimeoutMs: next.streamIdleTimeoutMs,
|
|
5674
|
-
responseHeaderTimeoutMs: next.responseHeaderTimeoutMs,
|
|
5675
|
-
modelQueueMaxWaitMs: next.modelQueueMaxWaitMs,
|
|
5676
|
-
preserveThinking: next.preserveThinking,
|
|
5677
|
-
requestCompression: next.requestCompression,
|
|
5678
|
-
modelTransport: next.modelTransport
|
|
5679
|
-
};
|
|
5680
|
-
}
|
|
5769
|
+
if (runtime.update(next)) authorization.activateRegion(runtime.config.region);
|
|
5681
5770
|
catalog.observeTransport();
|
|
5682
5771
|
adapter.replaceModels(next.models);
|
|
5683
5772
|
registration.replace([providerQoder]);
|
|
@@ -5716,7 +5805,12 @@ function apply(ctx, input = {}) {
|
|
|
5716
5805
|
const discoverModels = async (signal, suppliedPat) => {
|
|
5717
5806
|
const snapshot = resolveConfig();
|
|
5718
5807
|
const normalizedPat = suppliedPat?.trim();
|
|
5719
|
-
const transport =
|
|
5808
|
+
const transport = runtime.transport;
|
|
5809
|
+
if (normalizedPat) {
|
|
5810
|
+
const view = await authorization.describe(snapshot.region);
|
|
5811
|
+
const managedPat = await resolveManagedQoderPat(ctx.credentials);
|
|
5812
|
+
if (view.method !== "pat" || normalizedPat !== managedPat.trim() || transport !== runtime.transport) return createTransport(transportConfig(snapshot), () => Promise.resolve(normalizedPat)).discoverModels(signal);
|
|
5813
|
+
}
|
|
5720
5814
|
return catalog.discover(signal, transport);
|
|
5721
5815
|
};
|
|
5722
5816
|
ctx.llm.registerModelDiscovery(settingsNamespace, async (request, signal) => {
|
|
@@ -5785,7 +5879,7 @@ function apply(ctx, input = {}) {
|
|
|
5785
5879
|
}
|
|
5786
5880
|
const force = typeof payload === "object" && payload !== null && "force" in payload ? payload.force === true : false;
|
|
5787
5881
|
logger?.debug?.("[Qoder RPC] Reading subscriber account", { force });
|
|
5788
|
-
const outcome = await executeRpc("load Qoder account", () =>
|
|
5882
|
+
const outcome = await executeRpc("load Qoder account", () => runtime.transport.readAccount({
|
|
5789
5883
|
force,
|
|
5790
5884
|
signal
|
|
5791
5885
|
}), signal, logger);
|
|
@@ -5800,7 +5894,7 @@ function apply(ctx, input = {}) {
|
|
|
5800
5894
|
ctx.inject(["web"], (webCtx) => {
|
|
5801
5895
|
const searchProvider = new QoderSearchProvider({
|
|
5802
5896
|
ctx,
|
|
5803
|
-
resolveTransport: () =>
|
|
5897
|
+
resolveTransport: () => runtime.transport,
|
|
5804
5898
|
getWebSearchMode: () => resolveConfig().webSearchMode ?? "auto"
|
|
5805
5899
|
});
|
|
5806
5900
|
webCtx.web.registerSearchProvider(searchProvider);
|