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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +405 -352
  3. 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
- return {
1628
- ...credentials,
1629
- expires: Date.now() + 60 * 60 * 1e3
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
- } catch {
1691
+ lastError = `HTTP ${response.status} ${response.statusText}`;
1692
+ } catch (error) {
1693
+ lastError = error instanceof Error ? error.message : String(error);
1678
1694
  }
1679
- const refreshedFallback = {
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 resolveAccessToken(providerID, ctx) {
2076
+ async function resolveUsageIdentity(providerID, mode, ctx, withIdentity = true) {
2077
+ let access;
2058
2078
  try {
2059
- const fromRegistry = await ctx?.modelRegistry?.getApiKeyForProvider(providerID);
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
- return getCachedCredentials2("", providerID)?.access || void 0;
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 accessToken = await resolveAccessToken(providerID, ctx);
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 accessToken = options?.apiKey;
2684
- if (!accessToken) {
2685
- throw new Error(
2686
- 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."
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
- const stablePart = stableHash("qoder-session", userID, qoderModel);
2712
- const sessionID = options?.sessionId ? `${stablePart}-${options.sessionId}` : `${stablePart}-${crypto3.randomUUID()}`;
2713
- let maxTokens = MAX_OUTPUT_TOKENS;
2714
- if (options?.maxTokens && options.maxTokens < maxTokens) {
2715
- maxTokens = options.maxTokens;
2716
- }
2717
- const toolsRaw = resolved.tools.length > 0 ? transformTools(resolved.tools) : void 0;
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
- } else {
2731
- parameters.enable_thinking = false;
2732
- }
2733
- const reqBody = {
2734
- request_id: crypto3.randomUUID(),
2735
- request_set_id: recordID,
2736
- chat_record_id: recordID,
2737
- session_id: sessionID,
2738
- stream: true,
2739
- chat_task: "FREE_INPUT",
2740
- is_reply: true,
2741
- is_retry: false,
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
- const bodyBytes = Buffer.from(JSON.stringify(reqBody));
2784
- const encodedBytes = qoderEncodeBody(bodyBytes);
2785
- const chatURL = getQoderChatURL(providerMode);
2786
- const headers = buildAuthHeaders(encodedBytes, chatURL, {
2787
- userID,
2788
- authToken: accessToken,
2789
- name,
2790
- email,
2791
- machineID
2792
- });
2793
- const modelSource = modelConfig.source || "system";
2794
- let queuedAttempt = 0;
2795
- let reader;
2796
- let decoder;
2797
- let buffer = "";
2798
- let bufferStart = 0;
2799
- for (; ; ) {
2800
- const response = await fetch(chatURL, {
2801
- method: "POST",
2802
- headers: {
2803
- "Content-Type": "application/json",
2804
- Accept: "text/event-stream",
2805
- "Cache-Control": "no-cache",
2806
- "Accept-Encoding": "identity",
2807
- "X-Model-Key": qoderModel,
2808
- "X-Model-Source": modelSource,
2809
- ...headers
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
- body: encodedBytes,
2812
- signal: options?.signal
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
- if (!response.ok) {
2815
- const errText = await response.text();
2816
- throw new Error(formatQoderStreamError(response.status, errText));
2817
- }
2818
- const body = response.body;
2819
- if (!body) throw new Error("No response body");
2820
- reader = new PeekableReader(body.getReader());
2821
- decoder = new TextDecoder();
2822
- buffer = "";
2823
- bufferStart = 0;
2824
- const queue = await peekQueueNotice(reader, decoder);
2825
- if (queue) {
2826
- await reader.cancel().catch(() => {
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 (queuedAttempt >= MAX_QUEUE_RETRIES) {
2829
- throw new Error(
2830
- `Qoder is at capacity: the request stayed queued after ${queuedAttempt + 1} attempts (~${queue.waitTimeSeconds}s wait reported). Try again shortly.`
2831
- );
2853
+ if (!response.ok) {
2854
+ const errText = await response.text();
2855
+ throw new Error(formatQoderStreamError(response.status, errText));
2832
2856
  }
2833
- queuedAttempt++;
2834
- await sleep(queue.retryAfterMs, options?.signal);
2835
- continue;
2836
- }
2837
- break;
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
- buffer += decoder.decode(value, { stream: true });
2856
- while (true) {
2857
- const lineEnd = buffer.indexOf("\n", bufferStart);
2858
- if (lineEnd === -1) break;
2859
- const line = buffer.substring(bufferStart, lineEnd).trim();
2860
- bufferStart = lineEnd + 1;
2861
- if (!line.startsWith("data:")) continue;
2862
- const dataStr = line.substring(5).trim();
2863
- if (dataStr === "[DONE]") {
2864
- sawDone = true;
2865
- break;
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
- try {
2868
- const envelope = JSON.parse(dataStr);
2869
- if (envelope.statusCodeValue && envelope.statusCodeValue !== 200) {
2870
- throw new Error(formatQoderStreamError(envelope.statusCodeValue, envelope.body));
2871
- }
2872
- const innerStr = envelope.body;
2873
- if (innerStr === "[DONE]") {
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
- if (!innerStr) continue;
2878
- const inner = JSON.parse(innerStr);
2879
- if (inner.id) output.responseId = inner.id;
2880
- if (inner.model) output.responseModel = inner.model;
2881
- if (inner.usage) {
2882
- const u = inner.usage;
2883
- const promptTokens = u.prompt_tokens ?? 0;
2884
- const cacheReadTokens = u.prompt_tokens_details?.cached_tokens ?? 0;
2885
- const cacheWriteTokens = u.prompt_tokens_details?.cache_write_tokens ?? 0;
2886
- output.usage.input = Math.max(0, promptTokens - cacheReadTokens - cacheWriteTokens);
2887
- output.usage.output = u.completion_tokens ?? 0;
2888
- output.usage.totalTokens = u.total_tokens ?? 0;
2889
- output.usage.cacheRead = cacheReadTokens;
2890
- output.usage.cacheWrite = cacheWriteTokens;
2891
- }
2892
- if (inner.choices && inner.choices.length > 0) {
2893
- const choice = inner.choices[0];
2894
- const delta = choice.delta;
2895
- if (delta) {
2896
- if (delta.reasoning_content) {
2897
- rawReasoningTail = (rawReasoningTail + delta.reasoning_content).slice(-512);
2898
- const reasoningChunk = stripDsmlResidue(stripThinkingTags(delta.reasoning_content));
2899
- if (reasoningChunk) {
2900
- if (thinkingBlockIndex === -1) {
2901
- thinkingBlockIndex = output.content.length;
2902
- output.content.push({ type: "thinking", thinking: "" });
2903
- stream.push({ type: "thinking_start", contentIndex: thinkingBlockIndex, partial: output });
2904
- }
2905
- const block = output.content[thinkingBlockIndex];
2906
- block.thinking += reasoningChunk;
2907
- stream.push({
2908
- type: "thinking_delta",
2909
- contentIndex: thinkingBlockIndex,
2910
- delta: reasoningChunk,
2911
- partial: output
2912
- });
2913
- }
2914
- }
2915
- if (delta.content) {
2916
- if (thinkingBlockIndex !== -1) {
2917
- const block = output.content[thinkingBlockIndex];
2918
- stream.push({
2919
- type: "thinking_end",
2920
- contentIndex: thinkingBlockIndex,
2921
- content: block.thinking,
2922
- partial: output
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
- if (delta.tool_calls && Array.isArray(delta.tool_calls)) {
2945
- for (const tc of delta.tool_calls) {
2946
- const idx = tc.index ?? 0;
2947
- if (!toolCallsState[idx]) {
2948
- toolCallsState[idx] = { arguments: "", id: "", name: "", contentIndex: 0 };
2949
- }
2950
- const state = toolCallsState[idx];
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
- stream.push({ type: "toolcall_start", contentIndex: state.contentIndex, partial: output });
2963
+ thinkingBlockIndex = -1;
2963
2964
  }
2964
- if (state.emittedStart) {
2965
- const block = output.content[state.contentIndex];
2966
- block.id = state.id;
2967
- block.name = state.name;
2968
- }
2969
- if (tc.function?.arguments) {
2970
- const argDelta = tc.function.arguments;
2971
- state.arguments += argDelta;
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: "toolcall_delta",
2974
- contentIndex: state.contentIndex,
2975
- delta: argDelta,
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
- if (choice.finish_reason) {
2983
- output.stopReason = choice.finish_reason;
2984
- }
2985
- }
2986
- } catch (e) {
2987
- if (e instanceof SyntaxError) {
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
- continue;
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
- for (const state of toolCallsState) {
3012
- if (state?.emittedStart && !state.emittedEnd) {
3013
- state.emittedEnd = true;
3014
- let args = {};
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: "toolcall_end",
3023
- contentIndex: state.contentIndex,
3024
- toolCall: {
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
- if (toolCallsState.some((state) => state?.emittedStart)) {
3035
- output.stopReason = "toolUse";
3036
- }
3037
- if (isDegenerateDsmlTurn(output, rawReasoningTail)) {
3038
- output.stopReason = "error";
3039
- output.errorMessage = "Qoder server error: degenerate model output (unparseable DSML tool-call markup, no tool call executed)";
3040
- stream.push({ type: "error", reason: "error", error: output });
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
- return;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-provider-qoder-cn",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Pi extension for Qoder CN — PAT/OAuth auth, /qoder-cn.usage quota command, and enterprise VPC endpoint support",
5
5
  "type": "module",
6
6
  "license": "MIT",