pi-provider-qoder-cn 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/index.js +405 -352
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,6 +74,7 @@ Qoder CN Plan (personal_standard)
|
|
|
74
74
|
Overall [███░░░░░░░░░░░░░░░░░░] 12.5% used
|
|
75
75
|
Plan quota [███░░░░░░░░░░░░░░░░░░] 250 / 2000 credits 12.5% 1750 left
|
|
76
76
|
Add-on [░░░░░░░░░░░░░░░░░░░░░] 0 / 700 credits 0% 700 left
|
|
77
|
+
User someone · someone@example.com
|
|
77
78
|
Expires never
|
|
78
79
|
Manage https://qoder.com.cn/account/usage
|
|
79
80
|
```
|
package/dist/index.js
CHANGED
|
@@ -1100,6 +1100,16 @@ var init_catalog = __esm({
|
|
|
1100
1100
|
});
|
|
1101
1101
|
|
|
1102
1102
|
// src/auth/pat.ts
|
|
1103
|
+
var pat_exports = {};
|
|
1104
|
+
__export(pat_exports, {
|
|
1105
|
+
PAT_REFRESH_PREFIX: () => PAT_REFRESH_PREFIX,
|
|
1106
|
+
credentialsFromPat: () => credentialsFromPat,
|
|
1107
|
+
decodePatRefresh: () => decodePatRefresh,
|
|
1108
|
+
encodePatRefresh: () => encodePatRefresh,
|
|
1109
|
+
exchangeJobToken: () => exchangeJobToken,
|
|
1110
|
+
fetchUserInfo: () => fetchUserInfo,
|
|
1111
|
+
isPatRefresh: () => isPatRefresh
|
|
1112
|
+
});
|
|
1103
1113
|
function isPatRefresh(refresh) {
|
|
1104
1114
|
return refresh.startsWith(`${PAT_REFRESH_PREFIX}|`);
|
|
1105
1115
|
}
|
|
@@ -1465,7 +1475,8 @@ __export(oauth_exports, {
|
|
|
1465
1475
|
getQoderPatForMode: () => getQoderPatForMode,
|
|
1466
1476
|
loginQoderForMode: () => loginQoderForMode,
|
|
1467
1477
|
refreshQoderTokenForMode: () => refreshQoderTokenForMode,
|
|
1468
|
-
resolveQoderIdentity: () => resolveQoderIdentity
|
|
1478
|
+
resolveQoderIdentity: () => resolveQoderIdentity,
|
|
1479
|
+
saveCredentialsToAuthFile: () => saveCredentialsToAuthFile
|
|
1469
1480
|
});
|
|
1470
1481
|
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1471
1482
|
import { homedir as homedir4 } from "node:os";
|
|
@@ -1621,14 +1632,16 @@ async function refreshQoderTokenForMode(credentials, mode) {
|
|
|
1621
1632
|
updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email, mode).catch(() => {
|
|
1622
1633
|
});
|
|
1623
1634
|
return refreshed;
|
|
1624
|
-
} catch {
|
|
1635
|
+
} catch (error) {
|
|
1636
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1637
|
+
throw new Error(
|
|
1638
|
+
`Qoder CN credential refresh failed (PAT re-exchange): ${detail}. If this persists, the PAT may be revoked \u2014 run /login qoder-cn.`
|
|
1639
|
+
);
|
|
1625
1640
|
}
|
|
1626
1641
|
}
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
// extend 1 hour to retry later
|
|
1631
|
-
};
|
|
1642
|
+
throw new Error(
|
|
1643
|
+
"Qoder CN credential refresh failed: the stored refresh chain carries no PAT. Run /login qoder-cn."
|
|
1644
|
+
);
|
|
1632
1645
|
}
|
|
1633
1646
|
const parts = credentials.refresh.split("|");
|
|
1634
1647
|
const refreshToken = parts[0] || "";
|
|
@@ -1638,6 +1651,7 @@ async function refreshQoderTokenForMode(credentials, mode) {
|
|
|
1638
1651
|
const prevName = prev.name || "";
|
|
1639
1652
|
const prevEmail = prev.email || "";
|
|
1640
1653
|
const refreshURL = getQoderRefreshURL(mode);
|
|
1654
|
+
let lastError = "unknown error";
|
|
1641
1655
|
try {
|
|
1642
1656
|
const response = await fetch(refreshURL, {
|
|
1643
1657
|
method: "POST",
|
|
@@ -1674,14 +1688,11 @@ async function refreshQoderTokenForMode(credentials, mode) {
|
|
|
1674
1688
|
});
|
|
1675
1689
|
return refreshed;
|
|
1676
1690
|
}
|
|
1677
|
-
|
|
1691
|
+
lastError = `HTTP ${response.status} ${response.statusText}`;
|
|
1692
|
+
} catch (error) {
|
|
1693
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
1678
1694
|
}
|
|
1679
|
-
|
|
1680
|
-
...credentials,
|
|
1681
|
-
expires: Date.now() + 60 * 60 * 1e3
|
|
1682
|
-
// extend for 1 hour
|
|
1683
|
-
};
|
|
1684
|
-
return refreshedFallback;
|
|
1695
|
+
throw new Error(`Qoder CN token refresh failed (${lastError}). Run /login qoder-cn.`);
|
|
1685
1696
|
}
|
|
1686
1697
|
var AuthStorage2, identityCache, authFileMem;
|
|
1687
1698
|
var init_oauth = __esm({
|
|
@@ -1870,6 +1881,10 @@ function formatQoderStreamError(statusCode, rawBody, now = Date.now()) {
|
|
|
1870
1881
|
const truncated = bodyStr.length > 500 ? `${bodyStr.slice(0, 500)}...` : bodyStr;
|
|
1871
1882
|
return `Upstream status ${statusCode}: ${truncated}`;
|
|
1872
1883
|
}
|
|
1884
|
+
function isCredentialExpiredError(error) {
|
|
1885
|
+
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
1886
|
+
return message.includes("\u9519\u8BEF\u7801 105") || message.includes("\u51ED\u8BC1\u5931\u6548") || /token expired|login expired/i.test(message);
|
|
1887
|
+
}
|
|
1873
1888
|
|
|
1874
1889
|
// src/commands/usage.ts
|
|
1875
1890
|
init_region();
|
|
@@ -2017,6 +2032,10 @@ function formatQoderUsage(raw, mode, now = Date.now(), options = {}) {
|
|
|
2017
2032
|
}
|
|
2018
2033
|
if (!showUserQuota && !showAddOnQuota && !showOrg) notes.push({ label: "Buckets", value: "none returned" });
|
|
2019
2034
|
if (raw?.isPlanQuotaProrated) notes.push({ label: "Note", value: "plan quota is prorated" });
|
|
2035
|
+
const user = options.user;
|
|
2036
|
+
if (user && (user.name || user.email)) {
|
|
2037
|
+
notes.push({ label: "User", value: [user.name, user.email].filter(Boolean).join(" \xB7 ") });
|
|
2038
|
+
}
|
|
2020
2039
|
if (isPersonalStandard || !showOrg && formatResetTime(raw?.expiresAt, now) === "never") {
|
|
2021
2040
|
notes.push({ label: "Daily reset", value: formatDailyResetCountdown(now) });
|
|
2022
2041
|
}
|
|
@@ -2054,14 +2073,30 @@ async function fetchQoderQuota(accessToken, mode) {
|
|
|
2054
2073
|
if (!data) throw new Error("Qoder usage response was not JSON.");
|
|
2055
2074
|
return data;
|
|
2056
2075
|
}
|
|
2057
|
-
async function
|
|
2076
|
+
async function resolveUsageIdentity(providerID, mode, ctx, withIdentity = true) {
|
|
2077
|
+
let access;
|
|
2058
2078
|
try {
|
|
2059
|
-
|
|
2060
|
-
if (fromRegistry) return fromRegistry;
|
|
2079
|
+
access = await ctx?.modelRegistry?.getApiKeyForProvider(providerID) || void 0;
|
|
2061
2080
|
} catch {
|
|
2062
2081
|
}
|
|
2063
2082
|
const { getCachedCredentials: getCachedCredentials2 } = await Promise.resolve().then(() => (init_oauth(), oauth_exports));
|
|
2064
|
-
|
|
2083
|
+
const stored = getCachedCredentials2("", providerID);
|
|
2084
|
+
access = access || stored?.access || void 0;
|
|
2085
|
+
if (!access) return void 0;
|
|
2086
|
+
if (!withIdentity) return { access, name: "", email: "" };
|
|
2087
|
+
const storedMatches = stored?.access === access;
|
|
2088
|
+
let name = storedMatches && stored?.name || "";
|
|
2089
|
+
let email = storedMatches && stored?.email || "";
|
|
2090
|
+
if (!name && !email) {
|
|
2091
|
+
try {
|
|
2092
|
+
const { fetchUserInfo: fetchUserInfo2 } = await Promise.resolve().then(() => (init_pat(), pat_exports));
|
|
2093
|
+
const info = await fetchUserInfo2(access, mode);
|
|
2094
|
+
name = info.name || "";
|
|
2095
|
+
email = info.email || "";
|
|
2096
|
+
} catch {
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
return { access, name, email };
|
|
2065
2100
|
}
|
|
2066
2101
|
var FORMAT_ARGS = /* @__PURE__ */ new Set(["json", "raw", "debug"]);
|
|
2067
2102
|
var PLAIN_ARGS = /* @__PURE__ */ new Set(["plain", "no-color", "nocolor"]);
|
|
@@ -2077,14 +2112,15 @@ async function runUsageCommand(mode, args, ctx) {
|
|
|
2077
2112
|
const providerID = region.providerID;
|
|
2078
2113
|
const wantsRaw = FORMAT_ARGS.has((args || "").trim().toLowerCase());
|
|
2079
2114
|
try {
|
|
2080
|
-
const
|
|
2115
|
+
const identity = await resolveUsageIdentity(providerID, mode, ctx, !wantsRaw);
|
|
2116
|
+
const accessToken = identity?.access;
|
|
2081
2117
|
if (!accessToken) {
|
|
2082
2118
|
ctx?.ui?.notify(`No ${providerID} credentials. Run /login ${providerID} first.`, "warning");
|
|
2083
2119
|
return;
|
|
2084
2120
|
}
|
|
2085
2121
|
const raw = await fetchQoderQuota(accessToken, mode);
|
|
2086
2122
|
const color = !wantsRaw && shouldColorize(args, { hasUI: Boolean(ctx?.ui) });
|
|
2087
|
-
const output = wantsRaw ? JSON.stringify(raw, null, 2) : formatQoderUsage(raw, mode, Date.now(), { color }).lines.join("\n");
|
|
2123
|
+
const output = wantsRaw ? JSON.stringify(raw, null, 2) : formatQoderUsage(raw, mode, Date.now(), { color, user: identity }).lines.join("\n");
|
|
2088
2124
|
ctx?.ui?.notify(output, "info");
|
|
2089
2125
|
if (!ctx?.ui) console.log(output);
|
|
2090
2126
|
} catch (error) {
|
|
@@ -2544,6 +2580,8 @@ function transformMessagesForQoder(messages) {
|
|
|
2544
2580
|
}
|
|
2545
2581
|
|
|
2546
2582
|
// src/protocol/stream.ts
|
|
2583
|
+
var lastForcedHealAt = 0;
|
|
2584
|
+
var HEAL_COOLDOWN_MS = 6e4;
|
|
2547
2585
|
function stableHash(prefix, ...inputs) {
|
|
2548
2586
|
const hash = crypto3.createHash("sha256");
|
|
2549
2587
|
hash.update(prefix);
|
|
@@ -2680,373 +2718,388 @@ function streamQoder(model, context, options) {
|
|
|
2680
2718
|
try {
|
|
2681
2719
|
const providerMode = model.provider === "qoder-cn" ? "cn" : "global";
|
|
2682
2720
|
const region = getQoderRegionConfig(providerMode);
|
|
2683
|
-
const
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
const ident = await resolveQoderIdentity(accessToken, model.provider, providerMode);
|
|
2690
|
-
const userID = ident.userID || "qoder-user";
|
|
2691
|
-
const name = ident.name || region.userNameFallback;
|
|
2692
|
-
const email = ident.email || region.userEmailFallback;
|
|
2693
|
-
const machineID = ident.machineID || getMachineId();
|
|
2694
|
-
const modelConfig = getCachedModelConfig(model.id, providerMode);
|
|
2695
|
-
if (!modelConfig?.key) {
|
|
2696
|
-
throw new Error(`Unknown Qoder model id: ${model.id}`);
|
|
2697
|
-
}
|
|
2698
|
-
const qoderModel = modelConfig.key;
|
|
2699
|
-
const isReasoning = !!modelConfig.is_reasoning;
|
|
2700
|
-
const resolved = resolveRequestContext(PiAi, context);
|
|
2701
|
-
const normalizedMessages = transformMessagesForQoder(resolved.messages);
|
|
2702
|
-
const systemText = resolved.systemText;
|
|
2703
|
-
let lastUserText = "";
|
|
2704
|
-
for (let i = normalizedMessages.length - 1; i >= 0; i--) {
|
|
2705
|
-
if (normalizedMessages[i].role === "user") {
|
|
2706
|
-
const content = normalizedMessages[i].content;
|
|
2707
|
-
lastUserText = typeof content === "string" ? content : Array.isArray(content) ? content.map((c) => "text" in c ? c.text : "").join("") : "";
|
|
2708
|
-
break;
|
|
2721
|
+
const runAttempt = async (apiKey) => {
|
|
2722
|
+
const accessToken = apiKey ?? options?.apiKey;
|
|
2723
|
+
if (!accessToken) {
|
|
2724
|
+
throw new Error(
|
|
2725
|
+
providerMode === "cn" ? "Qoder CN credentials not set. Run /login qoder-cn or set QODERCN_PERSONAL_ACCESS_TOKEN." : "Qoder credentials not set. Run /login qoder or set QODER_PERSONAL_ACCESS_TOKEN."
|
|
2726
|
+
);
|
|
2709
2727
|
}
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
const recordID = stableChatRecordID(qoderModel, normalizedMessages, toolsRaw, maxTokens);
|
|
2719
|
-
const requestedLevel = options?.reasoning;
|
|
2720
|
-
const clamped = requestedLevel ? clampThinkingLevel(model, requestedLevel) : void 0;
|
|
2721
|
-
const reasoningLevel = clamped === "off" ? void 0 : clamped;
|
|
2722
|
-
const parameters = { max_tokens: maxTokens };
|
|
2723
|
-
if (reasoningLevel) {
|
|
2724
|
-
parameters.enable_thinking = true;
|
|
2725
|
-
const mapped = model.thinkingLevelMap?.[reasoningLevel];
|
|
2726
|
-
const effort = mapped && mapped !== "enabled" && mapped !== "disabled" ? mapped : reasoningLevel;
|
|
2727
|
-
if (modelConfig?.thinking_config?.enabled?.efforts && typeof effort === "string") {
|
|
2728
|
-
parameters.reasoning_effort = effort;
|
|
2728
|
+
const ident = await resolveQoderIdentity(accessToken, model.provider, providerMode);
|
|
2729
|
+
const userID = ident.userID || "qoder-user";
|
|
2730
|
+
const name = ident.name || region.userNameFallback;
|
|
2731
|
+
const email = ident.email || region.userEmailFallback;
|
|
2732
|
+
const machineID = ident.machineID || getMachineId();
|
|
2733
|
+
const modelConfig = getCachedModelConfig(model.id, providerMode);
|
|
2734
|
+
if (!modelConfig?.key) {
|
|
2735
|
+
throw new Error(`Unknown Qoder model id: ${model.id}`);
|
|
2729
2736
|
}
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
source: 1,
|
|
2743
|
-
version: "3",
|
|
2744
|
-
session_type: "qodercli",
|
|
2745
|
-
agent_id: "agent_common",
|
|
2746
|
-
task_id: "common",
|
|
2747
|
-
code_language: "",
|
|
2748
|
-
chat_prompt: "",
|
|
2749
|
-
image_urls: null,
|
|
2750
|
-
aliyun_user_type: "",
|
|
2751
|
-
// Qoder's server ignores the top-level `system` field (verified: the
|
|
2752
|
-
// model never sees it). Inject the system prompt as a leading
|
|
2753
|
-
// role:system message instead, which the server does honor.
|
|
2754
|
-
system: "",
|
|
2755
|
-
messages: systemText ? [{ role: "system", content: systemText }, ...normalizedMessages] : normalizedMessages,
|
|
2756
|
-
tools: toolsRaw || [],
|
|
2757
|
-
parameters,
|
|
2758
|
-
chat_context: {
|
|
2759
|
-
chatPrompt: "",
|
|
2760
|
-
imageUrls: null,
|
|
2761
|
-
extra: {
|
|
2762
|
-
context: [],
|
|
2763
|
-
modelConfig: {
|
|
2764
|
-
key: qoderModel,
|
|
2765
|
-
is_reasoning: isReasoning
|
|
2766
|
-
},
|
|
2767
|
-
originalContent: lastUserText
|
|
2768
|
-
},
|
|
2769
|
-
features: [],
|
|
2770
|
-
text: lastUserText
|
|
2771
|
-
},
|
|
2772
|
-
model_config: modelConfig,
|
|
2773
|
-
business: {
|
|
2774
|
-
product: "cli",
|
|
2775
|
-
version: "1.0.0",
|
|
2776
|
-
type: "agent",
|
|
2777
|
-
stage: "start",
|
|
2778
|
-
id: crypto3.randomUUID(),
|
|
2779
|
-
name: lastUserText.substring(0, 30),
|
|
2780
|
-
begin_at: Date.now()
|
|
2737
|
+
const qoderModel = modelConfig.key;
|
|
2738
|
+
const isReasoning = !!modelConfig.is_reasoning;
|
|
2739
|
+
const resolved = resolveRequestContext(PiAi, context);
|
|
2740
|
+
const normalizedMessages = transformMessagesForQoder(resolved.messages);
|
|
2741
|
+
const systemText = resolved.systemText;
|
|
2742
|
+
let lastUserText = "";
|
|
2743
|
+
for (let i = normalizedMessages.length - 1; i >= 0; i--) {
|
|
2744
|
+
if (normalizedMessages[i].role === "user") {
|
|
2745
|
+
const content = normalizedMessages[i].content;
|
|
2746
|
+
lastUserText = typeof content === "string" ? content : Array.isArray(content) ? content.map((c) => "text" in c ? c.text : "").join("") : "";
|
|
2747
|
+
break;
|
|
2748
|
+
}
|
|
2781
2749
|
}
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2750
|
+
const stablePart = stableHash("qoder-session", userID, qoderModel);
|
|
2751
|
+
const sessionID = options?.sessionId ? `${stablePart}-${options.sessionId}` : `${stablePart}-${crypto3.randomUUID()}`;
|
|
2752
|
+
let maxTokens = MAX_OUTPUT_TOKENS;
|
|
2753
|
+
if (options?.maxTokens && options.maxTokens < maxTokens) {
|
|
2754
|
+
maxTokens = options.maxTokens;
|
|
2755
|
+
}
|
|
2756
|
+
const toolsRaw = resolved.tools.length > 0 ? transformTools(resolved.tools) : void 0;
|
|
2757
|
+
const recordID = stableChatRecordID(qoderModel, normalizedMessages, toolsRaw, maxTokens);
|
|
2758
|
+
const requestedLevel = options?.reasoning;
|
|
2759
|
+
const clamped = requestedLevel ? clampThinkingLevel(model, requestedLevel) : void 0;
|
|
2760
|
+
const reasoningLevel = clamped === "off" ? void 0 : clamped;
|
|
2761
|
+
const parameters = { max_tokens: maxTokens };
|
|
2762
|
+
if (reasoningLevel) {
|
|
2763
|
+
parameters.enable_thinking = true;
|
|
2764
|
+
const mapped = model.thinkingLevelMap?.[reasoningLevel];
|
|
2765
|
+
const effort = mapped && mapped !== "enabled" && mapped !== "disabled" ? mapped : reasoningLevel;
|
|
2766
|
+
if (modelConfig?.thinking_config?.enabled?.efforts && typeof effort === "string") {
|
|
2767
|
+
parameters.reasoning_effort = effort;
|
|
2768
|
+
}
|
|
2769
|
+
} else {
|
|
2770
|
+
parameters.enable_thinking = false;
|
|
2771
|
+
}
|
|
2772
|
+
const reqBody = {
|
|
2773
|
+
request_id: crypto3.randomUUID(),
|
|
2774
|
+
request_set_id: recordID,
|
|
2775
|
+
chat_record_id: recordID,
|
|
2776
|
+
session_id: sessionID,
|
|
2777
|
+
stream: true,
|
|
2778
|
+
chat_task: "FREE_INPUT",
|
|
2779
|
+
is_reply: true,
|
|
2780
|
+
is_retry: false,
|
|
2781
|
+
source: 1,
|
|
2782
|
+
version: "3",
|
|
2783
|
+
session_type: "qodercli",
|
|
2784
|
+
agent_id: "agent_common",
|
|
2785
|
+
task_id: "common",
|
|
2786
|
+
code_language: "",
|
|
2787
|
+
chat_prompt: "",
|
|
2788
|
+
image_urls: null,
|
|
2789
|
+
aliyun_user_type: "",
|
|
2790
|
+
// Qoder's server ignores the top-level `system` field (verified: the
|
|
2791
|
+
// model never sees it). Inject the system prompt as a leading
|
|
2792
|
+
// role:system message instead, which the server does honor.
|
|
2793
|
+
system: "",
|
|
2794
|
+
messages: systemText ? [{ role: "system", content: systemText }, ...normalizedMessages] : normalizedMessages,
|
|
2795
|
+
tools: toolsRaw || [],
|
|
2796
|
+
parameters,
|
|
2797
|
+
chat_context: {
|
|
2798
|
+
chatPrompt: "",
|
|
2799
|
+
imageUrls: null,
|
|
2800
|
+
extra: {
|
|
2801
|
+
context: [],
|
|
2802
|
+
modelConfig: {
|
|
2803
|
+
key: qoderModel,
|
|
2804
|
+
is_reasoning: isReasoning
|
|
2805
|
+
},
|
|
2806
|
+
originalContent: lastUserText
|
|
2807
|
+
},
|
|
2808
|
+
features: [],
|
|
2809
|
+
text: lastUserText
|
|
2810
2810
|
},
|
|
2811
|
-
|
|
2812
|
-
|
|
2811
|
+
model_config: modelConfig,
|
|
2812
|
+
business: {
|
|
2813
|
+
product: "cli",
|
|
2814
|
+
version: "1.0.0",
|
|
2815
|
+
type: "agent",
|
|
2816
|
+
stage: "start",
|
|
2817
|
+
id: crypto3.randomUUID(),
|
|
2818
|
+
name: lastUserText.substring(0, 30),
|
|
2819
|
+
begin_at: Date.now()
|
|
2820
|
+
}
|
|
2821
|
+
};
|
|
2822
|
+
const bodyBytes = Buffer.from(JSON.stringify(reqBody));
|
|
2823
|
+
const encodedBytes = qoderEncodeBody(bodyBytes);
|
|
2824
|
+
const chatURL = getQoderChatURL(providerMode);
|
|
2825
|
+
const headers = buildAuthHeaders(encodedBytes, chatURL, {
|
|
2826
|
+
userID,
|
|
2827
|
+
authToken: accessToken,
|
|
2828
|
+
name,
|
|
2829
|
+
email,
|
|
2830
|
+
machineID
|
|
2813
2831
|
});
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2832
|
+
const modelSource = modelConfig.source || "system";
|
|
2833
|
+
let queuedAttempt = 0;
|
|
2834
|
+
let reader;
|
|
2835
|
+
let decoder;
|
|
2836
|
+
let buffer = "";
|
|
2837
|
+
let bufferStart = 0;
|
|
2838
|
+
for (; ; ) {
|
|
2839
|
+
const response = await fetch(chatURL, {
|
|
2840
|
+
method: "POST",
|
|
2841
|
+
headers: {
|
|
2842
|
+
"Content-Type": "application/json",
|
|
2843
|
+
Accept: "text/event-stream",
|
|
2844
|
+
"Cache-Control": "no-cache",
|
|
2845
|
+
"Accept-Encoding": "identity",
|
|
2846
|
+
"X-Model-Key": qoderModel,
|
|
2847
|
+
"X-Model-Source": modelSource,
|
|
2848
|
+
...headers
|
|
2849
|
+
},
|
|
2850
|
+
body: encodedBytes,
|
|
2851
|
+
signal: options?.signal
|
|
2827
2852
|
});
|
|
2828
|
-
if (
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
);
|
|
2853
|
+
if (!response.ok) {
|
|
2854
|
+
const errText = await response.text();
|
|
2855
|
+
throw new Error(formatQoderStreamError(response.status, errText));
|
|
2832
2856
|
}
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
}
|
|
2839
|
-
if (!reader || !decoder) throw new Error("No response body");
|
|
2840
|
-
let contentBlockIndex = -1;
|
|
2841
|
-
let thinkingBlockIndex = -1;
|
|
2842
|
-
let rawReasoningTail = "";
|
|
2843
|
-
const toolCallsState = [];
|
|
2844
|
-
const thinkingEnabled = options?.reasoning !== false && options?.reasoning !== "off";
|
|
2845
|
-
const thinkingParser = thinkingEnabled ? new ThinkingTagParser(output, stream) : null;
|
|
2846
|
-
stream.push({ type: "start", partial: output });
|
|
2847
|
-
let sawDone = false;
|
|
2848
|
-
while (!sawDone) {
|
|
2849
|
-
const { done, value } = await reader.read();
|
|
2850
|
-
if (done) break;
|
|
2851
|
-
if (bufferStart > 0) {
|
|
2852
|
-
buffer = buffer.substring(bufferStart);
|
|
2857
|
+
const body = response.body;
|
|
2858
|
+
if (!body) throw new Error("No response body");
|
|
2859
|
+
reader = new PeekableReader(body.getReader());
|
|
2860
|
+
decoder = new TextDecoder();
|
|
2861
|
+
buffer = "";
|
|
2853
2862
|
bufferStart = 0;
|
|
2863
|
+
const queue = await peekQueueNotice(reader, decoder);
|
|
2864
|
+
if (queue) {
|
|
2865
|
+
await reader.cancel().catch(() => {
|
|
2866
|
+
});
|
|
2867
|
+
if (queuedAttempt >= MAX_QUEUE_RETRIES) {
|
|
2868
|
+
throw new Error(
|
|
2869
|
+
`Qoder is at capacity: the request stayed queued after ${queuedAttempt + 1} attempts (~${queue.waitTimeSeconds}s wait reported). Try again shortly.`
|
|
2870
|
+
);
|
|
2871
|
+
}
|
|
2872
|
+
queuedAttempt++;
|
|
2873
|
+
await sleep(queue.retryAfterMs, options?.signal);
|
|
2874
|
+
continue;
|
|
2875
|
+
}
|
|
2876
|
+
break;
|
|
2854
2877
|
}
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2878
|
+
if (!reader || !decoder) throw new Error("No response body");
|
|
2879
|
+
let contentBlockIndex = -1;
|
|
2880
|
+
let thinkingBlockIndex = -1;
|
|
2881
|
+
let rawReasoningTail = "";
|
|
2882
|
+
const toolCallsState = [];
|
|
2883
|
+
const thinkingEnabled = options?.reasoning !== false && options?.reasoning !== "off";
|
|
2884
|
+
const thinkingParser = thinkingEnabled ? new ThinkingTagParser(output, stream) : null;
|
|
2885
|
+
stream.push({ type: "start", partial: output });
|
|
2886
|
+
let sawDone = false;
|
|
2887
|
+
while (!sawDone) {
|
|
2888
|
+
const { done, value } = await reader.read();
|
|
2889
|
+
if (done) break;
|
|
2890
|
+
if (bufferStart > 0) {
|
|
2891
|
+
buffer = buffer.substring(bufferStart);
|
|
2892
|
+
bufferStart = 0;
|
|
2866
2893
|
}
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
if (
|
|
2894
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2895
|
+
while (true) {
|
|
2896
|
+
const lineEnd = buffer.indexOf("\n", bufferStart);
|
|
2897
|
+
if (lineEnd === -1) break;
|
|
2898
|
+
const line = buffer.substring(bufferStart, lineEnd).trim();
|
|
2899
|
+
bufferStart = lineEnd + 1;
|
|
2900
|
+
if (!line.startsWith("data:")) continue;
|
|
2901
|
+
const dataStr = line.substring(5).trim();
|
|
2902
|
+
if (dataStr === "[DONE]") {
|
|
2874
2903
|
sawDone = true;
|
|
2875
2904
|
break;
|
|
2876
2905
|
}
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
const
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
output.
|
|
2890
|
-
output.
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
});
|
|
2924
|
-
thinkingBlockIndex = -1;
|
|
2925
|
-
}
|
|
2926
|
-
if (thinkingParser) {
|
|
2927
|
-
thinkingParser.processChunk(delta.content);
|
|
2928
|
-
} else {
|
|
2929
|
-
if (contentBlockIndex === -1) {
|
|
2930
|
-
contentBlockIndex = output.content.length;
|
|
2931
|
-
output.content.push({ type: "text", text: "" });
|
|
2932
|
-
stream.push({ type: "text_start", contentIndex: contentBlockIndex, partial: output });
|
|
2906
|
+
try {
|
|
2907
|
+
const envelope = JSON.parse(dataStr);
|
|
2908
|
+
if (envelope.statusCodeValue && envelope.statusCodeValue !== 200) {
|
|
2909
|
+
throw new Error(formatQoderStreamError(envelope.statusCodeValue, envelope.body));
|
|
2910
|
+
}
|
|
2911
|
+
const innerStr = envelope.body;
|
|
2912
|
+
if (innerStr === "[DONE]") {
|
|
2913
|
+
sawDone = true;
|
|
2914
|
+
break;
|
|
2915
|
+
}
|
|
2916
|
+
if (!innerStr) continue;
|
|
2917
|
+
const inner = JSON.parse(innerStr);
|
|
2918
|
+
if (inner.id) output.responseId = inner.id;
|
|
2919
|
+
if (inner.model) output.responseModel = inner.model;
|
|
2920
|
+
if (inner.usage) {
|
|
2921
|
+
const u = inner.usage;
|
|
2922
|
+
const promptTokens = u.prompt_tokens ?? 0;
|
|
2923
|
+
const cacheReadTokens = u.prompt_tokens_details?.cached_tokens ?? 0;
|
|
2924
|
+
const cacheWriteTokens = u.prompt_tokens_details?.cache_write_tokens ?? 0;
|
|
2925
|
+
output.usage.input = Math.max(0, promptTokens - cacheReadTokens - cacheWriteTokens);
|
|
2926
|
+
output.usage.output = u.completion_tokens ?? 0;
|
|
2927
|
+
output.usage.totalTokens = u.total_tokens ?? 0;
|
|
2928
|
+
output.usage.cacheRead = cacheReadTokens;
|
|
2929
|
+
output.usage.cacheWrite = cacheWriteTokens;
|
|
2930
|
+
}
|
|
2931
|
+
if (inner.choices && inner.choices.length > 0) {
|
|
2932
|
+
const choice = inner.choices[0];
|
|
2933
|
+
const delta = choice.delta;
|
|
2934
|
+
if (delta) {
|
|
2935
|
+
if (delta.reasoning_content) {
|
|
2936
|
+
rawReasoningTail = (rawReasoningTail + delta.reasoning_content).slice(-512);
|
|
2937
|
+
const reasoningChunk = stripDsmlResidue(stripThinkingTags(delta.reasoning_content));
|
|
2938
|
+
if (reasoningChunk) {
|
|
2939
|
+
if (thinkingBlockIndex === -1) {
|
|
2940
|
+
thinkingBlockIndex = output.content.length;
|
|
2941
|
+
output.content.push({ type: "thinking", thinking: "" });
|
|
2942
|
+
stream.push({ type: "thinking_start", contentIndex: thinkingBlockIndex, partial: output });
|
|
2943
|
+
}
|
|
2944
|
+
const block = output.content[thinkingBlockIndex];
|
|
2945
|
+
block.thinking += reasoningChunk;
|
|
2946
|
+
stream.push({
|
|
2947
|
+
type: "thinking_delta",
|
|
2948
|
+
contentIndex: thinkingBlockIndex,
|
|
2949
|
+
delta: reasoningChunk,
|
|
2950
|
+
partial: output
|
|
2951
|
+
});
|
|
2933
2952
|
}
|
|
2934
|
-
const block = output.content[contentBlockIndex];
|
|
2935
|
-
block.text += delta.content;
|
|
2936
|
-
stream.push({
|
|
2937
|
-
type: "text_delta",
|
|
2938
|
-
contentIndex: contentBlockIndex,
|
|
2939
|
-
delta: delta.content,
|
|
2940
|
-
partial: output
|
|
2941
|
-
});
|
|
2942
2953
|
}
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
if (tc.id) state.id = tc.id;
|
|
2952
|
-
if (tc.function?.name) state.name = tc.function.name;
|
|
2953
|
-
if (state.emittedStart === void 0 && (state.id || state.name)) {
|
|
2954
|
-
state.emittedStart = true;
|
|
2955
|
-
state.contentIndex = output.content.length;
|
|
2956
|
-
output.content.push({
|
|
2957
|
-
type: "toolCall",
|
|
2958
|
-
id: state.id,
|
|
2959
|
-
name: state.name,
|
|
2960
|
-
arguments: {}
|
|
2954
|
+
if (delta.content) {
|
|
2955
|
+
if (thinkingBlockIndex !== -1) {
|
|
2956
|
+
const block = output.content[thinkingBlockIndex];
|
|
2957
|
+
stream.push({
|
|
2958
|
+
type: "thinking_end",
|
|
2959
|
+
contentIndex: thinkingBlockIndex,
|
|
2960
|
+
content: block.thinking,
|
|
2961
|
+
partial: output
|
|
2961
2962
|
});
|
|
2962
|
-
|
|
2963
|
+
thinkingBlockIndex = -1;
|
|
2963
2964
|
}
|
|
2964
|
-
if (
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2965
|
+
if (thinkingParser) {
|
|
2966
|
+
thinkingParser.processChunk(delta.content);
|
|
2967
|
+
} else {
|
|
2968
|
+
if (contentBlockIndex === -1) {
|
|
2969
|
+
contentBlockIndex = output.content.length;
|
|
2970
|
+
output.content.push({ type: "text", text: "" });
|
|
2971
|
+
stream.push({ type: "text_start", contentIndex: contentBlockIndex, partial: output });
|
|
2972
|
+
}
|
|
2973
|
+
const block = output.content[contentBlockIndex];
|
|
2974
|
+
block.text += delta.content;
|
|
2972
2975
|
stream.push({
|
|
2973
|
-
type: "
|
|
2974
|
-
contentIndex:
|
|
2975
|
-
delta:
|
|
2976
|
+
type: "text_delta",
|
|
2977
|
+
contentIndex: contentBlockIndex,
|
|
2978
|
+
delta: delta.content,
|
|
2976
2979
|
partial: output
|
|
2977
2980
|
});
|
|
2978
2981
|
}
|
|
2979
2982
|
}
|
|
2983
|
+
if (delta.tool_calls && Array.isArray(delta.tool_calls)) {
|
|
2984
|
+
for (const tc of delta.tool_calls) {
|
|
2985
|
+
const idx = tc.index ?? 0;
|
|
2986
|
+
if (!toolCallsState[idx]) {
|
|
2987
|
+
toolCallsState[idx] = { arguments: "", id: "", name: "", contentIndex: 0 };
|
|
2988
|
+
}
|
|
2989
|
+
const state = toolCallsState[idx];
|
|
2990
|
+
if (tc.id) state.id = tc.id;
|
|
2991
|
+
if (tc.function?.name) state.name = tc.function.name;
|
|
2992
|
+
if (state.emittedStart === void 0 && (state.id || state.name)) {
|
|
2993
|
+
state.emittedStart = true;
|
|
2994
|
+
state.contentIndex = output.content.length;
|
|
2995
|
+
output.content.push({
|
|
2996
|
+
type: "toolCall",
|
|
2997
|
+
id: state.id,
|
|
2998
|
+
name: state.name,
|
|
2999
|
+
arguments: {}
|
|
3000
|
+
});
|
|
3001
|
+
stream.push({ type: "toolcall_start", contentIndex: state.contentIndex, partial: output });
|
|
3002
|
+
}
|
|
3003
|
+
if (state.emittedStart) {
|
|
3004
|
+
const block = output.content[state.contentIndex];
|
|
3005
|
+
block.id = state.id;
|
|
3006
|
+
block.name = state.name;
|
|
3007
|
+
}
|
|
3008
|
+
if (tc.function?.arguments) {
|
|
3009
|
+
const argDelta = tc.function.arguments;
|
|
3010
|
+
state.arguments += argDelta;
|
|
3011
|
+
stream.push({
|
|
3012
|
+
type: "toolcall_delta",
|
|
3013
|
+
contentIndex: state.contentIndex,
|
|
3014
|
+
delta: argDelta,
|
|
3015
|
+
partial: output
|
|
3016
|
+
});
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
if (choice.finish_reason) {
|
|
3022
|
+
output.stopReason = choice.finish_reason;
|
|
2980
3023
|
}
|
|
2981
3024
|
}
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
if (process.env.QODER_DEBUG) {
|
|
2989
|
-
console.error("[pi-provider-qoder] skipping malformed SSE line:", dataStr.slice(0, 200));
|
|
3025
|
+
} catch (e) {
|
|
3026
|
+
if (e instanceof SyntaxError) {
|
|
3027
|
+
if (process.env.QODER_DEBUG) {
|
|
3028
|
+
console.error("[pi-provider-qoder] skipping malformed SSE line:", dataStr.slice(0, 200));
|
|
3029
|
+
}
|
|
3030
|
+
continue;
|
|
2990
3031
|
}
|
|
2991
|
-
|
|
3032
|
+
throw e;
|
|
2992
3033
|
}
|
|
2993
|
-
throw e;
|
|
2994
3034
|
}
|
|
2995
3035
|
}
|
|
2996
|
-
|
|
2997
|
-
await reader.cancel().catch(() => {
|
|
2998
|
-
});
|
|
2999
|
-
if (thinkingParser) {
|
|
3000
|
-
thinkingParser.finalize();
|
|
3001
|
-
}
|
|
3002
|
-
if (thinkingBlockIndex !== -1) {
|
|
3003
|
-
const block = output.content[thinkingBlockIndex];
|
|
3004
|
-
stream.push({
|
|
3005
|
-
type: "thinking_end",
|
|
3006
|
-
contentIndex: thinkingBlockIndex,
|
|
3007
|
-
content: block.thinking,
|
|
3008
|
-
partial: output
|
|
3036
|
+
await reader.cancel().catch(() => {
|
|
3009
3037
|
});
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
try {
|
|
3016
|
-
args = JSON.parse(state.arguments || "{}");
|
|
3017
|
-
} catch {
|
|
3018
|
-
}
|
|
3019
|
-
const block = output.content[state.contentIndex];
|
|
3020
|
-
block.arguments = args;
|
|
3038
|
+
if (thinkingParser) {
|
|
3039
|
+
thinkingParser.finalize();
|
|
3040
|
+
}
|
|
3041
|
+
if (thinkingBlockIndex !== -1) {
|
|
3042
|
+
const block = output.content[thinkingBlockIndex];
|
|
3021
3043
|
stream.push({
|
|
3022
|
-
type: "
|
|
3023
|
-
contentIndex:
|
|
3024
|
-
|
|
3025
|
-
type: "toolCall",
|
|
3026
|
-
id: state.id,
|
|
3027
|
-
name: state.name,
|
|
3028
|
-
arguments: args
|
|
3029
|
-
},
|
|
3044
|
+
type: "thinking_end",
|
|
3045
|
+
contentIndex: thinkingBlockIndex,
|
|
3046
|
+
content: block.thinking,
|
|
3030
3047
|
partial: output
|
|
3031
3048
|
});
|
|
3032
3049
|
}
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3050
|
+
for (const state of toolCallsState) {
|
|
3051
|
+
if (state?.emittedStart && !state.emittedEnd) {
|
|
3052
|
+
state.emittedEnd = true;
|
|
3053
|
+
let args = {};
|
|
3054
|
+
try {
|
|
3055
|
+
args = JSON.parse(state.arguments || "{}");
|
|
3056
|
+
} catch {
|
|
3057
|
+
}
|
|
3058
|
+
const block = output.content[state.contentIndex];
|
|
3059
|
+
block.arguments = args;
|
|
3060
|
+
stream.push({
|
|
3061
|
+
type: "toolcall_end",
|
|
3062
|
+
contentIndex: state.contentIndex,
|
|
3063
|
+
toolCall: {
|
|
3064
|
+
type: "toolCall",
|
|
3065
|
+
id: state.id,
|
|
3066
|
+
name: state.name,
|
|
3067
|
+
arguments: args
|
|
3068
|
+
},
|
|
3069
|
+
partial: output
|
|
3070
|
+
});
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
if (toolCallsState.some((state) => state?.emittedStart)) {
|
|
3074
|
+
output.stopReason = "toolUse";
|
|
3075
|
+
}
|
|
3076
|
+
if (isDegenerateDsmlTurn(output, rawReasoningTail)) {
|
|
3077
|
+
output.stopReason = "error";
|
|
3078
|
+
output.errorMessage = "Qoder server error: degenerate model output (unparseable DSML tool-call markup, no tool call executed)";
|
|
3079
|
+
stream.push({ type: "error", reason: "error", error: output });
|
|
3080
|
+
stream.end();
|
|
3081
|
+
return;
|
|
3082
|
+
}
|
|
3083
|
+
stream.push({
|
|
3084
|
+
type: "done",
|
|
3085
|
+
reason: output.stopReason,
|
|
3086
|
+
message: output
|
|
3087
|
+
});
|
|
3041
3088
|
stream.end();
|
|
3042
|
-
|
|
3089
|
+
};
|
|
3090
|
+
try {
|
|
3091
|
+
await runAttempt(void 0);
|
|
3092
|
+
} catch (error) {
|
|
3093
|
+
if (model.provider !== "qoder-cn" || !isCredentialExpiredError(error) || Date.now() - lastForcedHealAt < HEAL_COOLDOWN_MS) {
|
|
3094
|
+
throw error;
|
|
3095
|
+
}
|
|
3096
|
+
lastForcedHealAt = Date.now();
|
|
3097
|
+
const stored = getCachedCredentials("", model.provider);
|
|
3098
|
+
if (!stored) throw error;
|
|
3099
|
+
const healed = await refreshQoderTokenForMode(stored, providerMode);
|
|
3100
|
+
saveCredentialsToAuthFile(model.provider, healed);
|
|
3101
|
+
await runAttempt(healed.access);
|
|
3043
3102
|
}
|
|
3044
|
-
stream.push({
|
|
3045
|
-
type: "done",
|
|
3046
|
-
reason: output.stopReason,
|
|
3047
|
-
message: output
|
|
3048
|
-
});
|
|
3049
|
-
stream.end();
|
|
3050
3103
|
} catch (e) {
|
|
3051
3104
|
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
3052
3105
|
output.errorMessage = e instanceof Error ? e.message : String(e);
|
package/package.json
CHANGED