pi-provider-qoder-cn 0.2.8 → 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 (2) hide show
  1. package/dist/index.js +368 -346
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1475,7 +1475,8 @@ __export(oauth_exports, {
1475
1475
  getQoderPatForMode: () => getQoderPatForMode,
1476
1476
  loginQoderForMode: () => loginQoderForMode,
1477
1477
  refreshQoderTokenForMode: () => refreshQoderTokenForMode,
1478
- resolveQoderIdentity: () => resolveQoderIdentity
1478
+ resolveQoderIdentity: () => resolveQoderIdentity,
1479
+ saveCredentialsToAuthFile: () => saveCredentialsToAuthFile
1479
1480
  });
1480
1481
  import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
1481
1482
  import { homedir as homedir4 } from "node:os";
@@ -1631,14 +1632,16 @@ async function refreshQoderTokenForMode(credentials, mode) {
1631
1632
  updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email, mode).catch(() => {
1632
1633
  });
1633
1634
  return refreshed;
1634
- } 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
+ );
1635
1640
  }
1636
1641
  }
1637
- return {
1638
- ...credentials,
1639
- expires: Date.now() + 60 * 60 * 1e3
1640
- // extend 1 hour to retry later
1641
- };
1642
+ throw new Error(
1643
+ "Qoder CN credential refresh failed: the stored refresh chain carries no PAT. Run /login qoder-cn."
1644
+ );
1642
1645
  }
1643
1646
  const parts = credentials.refresh.split("|");
1644
1647
  const refreshToken = parts[0] || "";
@@ -1648,6 +1651,7 @@ async function refreshQoderTokenForMode(credentials, mode) {
1648
1651
  const prevName = prev.name || "";
1649
1652
  const prevEmail = prev.email || "";
1650
1653
  const refreshURL = getQoderRefreshURL(mode);
1654
+ let lastError = "unknown error";
1651
1655
  try {
1652
1656
  const response = await fetch(refreshURL, {
1653
1657
  method: "POST",
@@ -1684,14 +1688,11 @@ async function refreshQoderTokenForMode(credentials, mode) {
1684
1688
  });
1685
1689
  return refreshed;
1686
1690
  }
1687
- } catch {
1691
+ lastError = `HTTP ${response.status} ${response.statusText}`;
1692
+ } catch (error) {
1693
+ lastError = error instanceof Error ? error.message : String(error);
1688
1694
  }
1689
- const refreshedFallback = {
1690
- ...credentials,
1691
- expires: Date.now() + 60 * 60 * 1e3
1692
- // extend for 1 hour
1693
- };
1694
- return refreshedFallback;
1695
+ throw new Error(`Qoder CN token refresh failed (${lastError}). Run /login qoder-cn.`);
1695
1696
  }
1696
1697
  var AuthStorage2, identityCache, authFileMem;
1697
1698
  var init_oauth = __esm({
@@ -1880,6 +1881,10 @@ function formatQoderStreamError(statusCode, rawBody, now = Date.now()) {
1880
1881
  const truncated = bodyStr.length > 500 ? `${bodyStr.slice(0, 500)}...` : bodyStr;
1881
1882
  return `Upstream status ${statusCode}: ${truncated}`;
1882
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
+ }
1883
1888
 
1884
1889
  // src/commands/usage.ts
1885
1890
  init_region();
@@ -2575,6 +2580,8 @@ function transformMessagesForQoder(messages) {
2575
2580
  }
2576
2581
 
2577
2582
  // src/protocol/stream.ts
2583
+ var lastForcedHealAt = 0;
2584
+ var HEAL_COOLDOWN_MS = 6e4;
2578
2585
  function stableHash(prefix, ...inputs) {
2579
2586
  const hash = crypto3.createHash("sha256");
2580
2587
  hash.update(prefix);
@@ -2711,373 +2718,388 @@ function streamQoder(model, context, options) {
2711
2718
  try {
2712
2719
  const providerMode = model.provider === "qoder-cn" ? "cn" : "global";
2713
2720
  const region = getQoderRegionConfig(providerMode);
2714
- const accessToken = options?.apiKey;
2715
- if (!accessToken) {
2716
- throw new Error(
2717
- 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."
2718
- );
2719
- }
2720
- const ident = await resolveQoderIdentity(accessToken, model.provider, providerMode);
2721
- const userID = ident.userID || "qoder-user";
2722
- const name = ident.name || region.userNameFallback;
2723
- const email = ident.email || region.userEmailFallback;
2724
- const machineID = ident.machineID || getMachineId();
2725
- const modelConfig = getCachedModelConfig(model.id, providerMode);
2726
- if (!modelConfig?.key) {
2727
- throw new Error(`Unknown Qoder model id: ${model.id}`);
2728
- }
2729
- const qoderModel = modelConfig.key;
2730
- const isReasoning = !!modelConfig.is_reasoning;
2731
- const resolved = resolveRequestContext(PiAi, context);
2732
- const normalizedMessages = transformMessagesForQoder(resolved.messages);
2733
- const systemText = resolved.systemText;
2734
- let lastUserText = "";
2735
- for (let i = normalizedMessages.length - 1; i >= 0; i--) {
2736
- if (normalizedMessages[i].role === "user") {
2737
- const content = normalizedMessages[i].content;
2738
- lastUserText = typeof content === "string" ? content : Array.isArray(content) ? content.map((c) => "text" in c ? c.text : "").join("") : "";
2739
- 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
+ );
2740
2727
  }
2741
- }
2742
- const stablePart = stableHash("qoder-session", userID, qoderModel);
2743
- const sessionID = options?.sessionId ? `${stablePart}-${options.sessionId}` : `${stablePart}-${crypto3.randomUUID()}`;
2744
- let maxTokens = MAX_OUTPUT_TOKENS;
2745
- if (options?.maxTokens && options.maxTokens < maxTokens) {
2746
- maxTokens = options.maxTokens;
2747
- }
2748
- const toolsRaw = resolved.tools.length > 0 ? transformTools(resolved.tools) : void 0;
2749
- const recordID = stableChatRecordID(qoderModel, normalizedMessages, toolsRaw, maxTokens);
2750
- const requestedLevel = options?.reasoning;
2751
- const clamped = requestedLevel ? clampThinkingLevel(model, requestedLevel) : void 0;
2752
- const reasoningLevel = clamped === "off" ? void 0 : clamped;
2753
- const parameters = { max_tokens: maxTokens };
2754
- if (reasoningLevel) {
2755
- parameters.enable_thinking = true;
2756
- const mapped = model.thinkingLevelMap?.[reasoningLevel];
2757
- const effort = mapped && mapped !== "enabled" && mapped !== "disabled" ? mapped : reasoningLevel;
2758
- if (modelConfig?.thinking_config?.enabled?.efforts && typeof effort === "string") {
2759
- 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}`);
2760
2736
  }
2761
- } else {
2762
- parameters.enable_thinking = false;
2763
- }
2764
- const reqBody = {
2765
- request_id: crypto3.randomUUID(),
2766
- request_set_id: recordID,
2767
- chat_record_id: recordID,
2768
- session_id: sessionID,
2769
- stream: true,
2770
- chat_task: "FREE_INPUT",
2771
- is_reply: true,
2772
- is_retry: false,
2773
- source: 1,
2774
- version: "3",
2775
- session_type: "qodercli",
2776
- agent_id: "agent_common",
2777
- task_id: "common",
2778
- code_language: "",
2779
- chat_prompt: "",
2780
- image_urls: null,
2781
- aliyun_user_type: "",
2782
- // Qoder's server ignores the top-level `system` field (verified: the
2783
- // model never sees it). Inject the system prompt as a leading
2784
- // role:system message instead, which the server does honor.
2785
- system: "",
2786
- messages: systemText ? [{ role: "system", content: systemText }, ...normalizedMessages] : normalizedMessages,
2787
- tools: toolsRaw || [],
2788
- parameters,
2789
- chat_context: {
2790
- chatPrompt: "",
2791
- imageUrls: null,
2792
- extra: {
2793
- context: [],
2794
- modelConfig: {
2795
- key: qoderModel,
2796
- is_reasoning: isReasoning
2797
- },
2798
- originalContent: lastUserText
2799
- },
2800
- features: [],
2801
- text: lastUserText
2802
- },
2803
- model_config: modelConfig,
2804
- business: {
2805
- product: "cli",
2806
- version: "1.0.0",
2807
- type: "agent",
2808
- stage: "start",
2809
- id: crypto3.randomUUID(),
2810
- name: lastUserText.substring(0, 30),
2811
- 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
+ }
2812
2749
  }
2813
- };
2814
- const bodyBytes = Buffer.from(JSON.stringify(reqBody));
2815
- const encodedBytes = qoderEncodeBody(bodyBytes);
2816
- const chatURL = getQoderChatURL(providerMode);
2817
- const headers = buildAuthHeaders(encodedBytes, chatURL, {
2818
- userID,
2819
- authToken: accessToken,
2820
- name,
2821
- email,
2822
- machineID
2823
- });
2824
- const modelSource = modelConfig.source || "system";
2825
- let queuedAttempt = 0;
2826
- let reader;
2827
- let decoder;
2828
- let buffer = "";
2829
- let bufferStart = 0;
2830
- for (; ; ) {
2831
- const response = await fetch(chatURL, {
2832
- method: "POST",
2833
- headers: {
2834
- "Content-Type": "application/json",
2835
- Accept: "text/event-stream",
2836
- "Cache-Control": "no-cache",
2837
- "Accept-Encoding": "identity",
2838
- "X-Model-Key": qoderModel,
2839
- "X-Model-Source": modelSource,
2840
- ...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
2841
2810
  },
2842
- body: encodedBytes,
2843
- 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
2844
2831
  });
2845
- if (!response.ok) {
2846
- const errText = await response.text();
2847
- throw new Error(formatQoderStreamError(response.status, errText));
2848
- }
2849
- const body = response.body;
2850
- if (!body) throw new Error("No response body");
2851
- reader = new PeekableReader(body.getReader());
2852
- decoder = new TextDecoder();
2853
- buffer = "";
2854
- bufferStart = 0;
2855
- const queue = await peekQueueNotice(reader, decoder);
2856
- if (queue) {
2857
- 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
2858
2852
  });
2859
- if (queuedAttempt >= MAX_QUEUE_RETRIES) {
2860
- throw new Error(
2861
- `Qoder is at capacity: the request stayed queued after ${queuedAttempt + 1} attempts (~${queue.waitTimeSeconds}s wait reported). Try again shortly.`
2862
- );
2853
+ if (!response.ok) {
2854
+ const errText = await response.text();
2855
+ throw new Error(formatQoderStreamError(response.status, errText));
2863
2856
  }
2864
- queuedAttempt++;
2865
- await sleep(queue.retryAfterMs, options?.signal);
2866
- continue;
2867
- }
2868
- break;
2869
- }
2870
- if (!reader || !decoder) throw new Error("No response body");
2871
- let contentBlockIndex = -1;
2872
- let thinkingBlockIndex = -1;
2873
- let rawReasoningTail = "";
2874
- const toolCallsState = [];
2875
- const thinkingEnabled = options?.reasoning !== false && options?.reasoning !== "off";
2876
- const thinkingParser = thinkingEnabled ? new ThinkingTagParser(output, stream) : null;
2877
- stream.push({ type: "start", partial: output });
2878
- let sawDone = false;
2879
- while (!sawDone) {
2880
- const { done, value } = await reader.read();
2881
- if (done) break;
2882
- if (bufferStart > 0) {
2883
- 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 = "";
2884
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;
2885
2877
  }
2886
- buffer += decoder.decode(value, { stream: true });
2887
- while (true) {
2888
- const lineEnd = buffer.indexOf("\n", bufferStart);
2889
- if (lineEnd === -1) break;
2890
- const line = buffer.substring(bufferStart, lineEnd).trim();
2891
- bufferStart = lineEnd + 1;
2892
- if (!line.startsWith("data:")) continue;
2893
- const dataStr = line.substring(5).trim();
2894
- if (dataStr === "[DONE]") {
2895
- sawDone = true;
2896
- 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;
2897
2893
  }
2898
- try {
2899
- const envelope = JSON.parse(dataStr);
2900
- if (envelope.statusCodeValue && envelope.statusCodeValue !== 200) {
2901
- throw new Error(formatQoderStreamError(envelope.statusCodeValue, envelope.body));
2902
- }
2903
- const innerStr = envelope.body;
2904
- 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]") {
2905
2903
  sawDone = true;
2906
2904
  break;
2907
2905
  }
2908
- if (!innerStr) continue;
2909
- const inner = JSON.parse(innerStr);
2910
- if (inner.id) output.responseId = inner.id;
2911
- if (inner.model) output.responseModel = inner.model;
2912
- if (inner.usage) {
2913
- const u = inner.usage;
2914
- const promptTokens = u.prompt_tokens ?? 0;
2915
- const cacheReadTokens = u.prompt_tokens_details?.cached_tokens ?? 0;
2916
- const cacheWriteTokens = u.prompt_tokens_details?.cache_write_tokens ?? 0;
2917
- output.usage.input = Math.max(0, promptTokens - cacheReadTokens - cacheWriteTokens);
2918
- output.usage.output = u.completion_tokens ?? 0;
2919
- output.usage.totalTokens = u.total_tokens ?? 0;
2920
- output.usage.cacheRead = cacheReadTokens;
2921
- output.usage.cacheWrite = cacheWriteTokens;
2922
- }
2923
- if (inner.choices && inner.choices.length > 0) {
2924
- const choice = inner.choices[0];
2925
- const delta = choice.delta;
2926
- if (delta) {
2927
- if (delta.reasoning_content) {
2928
- rawReasoningTail = (rawReasoningTail + delta.reasoning_content).slice(-512);
2929
- const reasoningChunk = stripDsmlResidue(stripThinkingTags(delta.reasoning_content));
2930
- if (reasoningChunk) {
2931
- if (thinkingBlockIndex === -1) {
2932
- thinkingBlockIndex = output.content.length;
2933
- output.content.push({ type: "thinking", thinking: "" });
2934
- stream.push({ type: "thinking_start", contentIndex: thinkingBlockIndex, partial: output });
2935
- }
2936
- const block = output.content[thinkingBlockIndex];
2937
- block.thinking += reasoningChunk;
2938
- stream.push({
2939
- type: "thinking_delta",
2940
- contentIndex: thinkingBlockIndex,
2941
- delta: reasoningChunk,
2942
- partial: output
2943
- });
2944
- }
2945
- }
2946
- if (delta.content) {
2947
- if (thinkingBlockIndex !== -1) {
2948
- const block = output.content[thinkingBlockIndex];
2949
- stream.push({
2950
- type: "thinking_end",
2951
- contentIndex: thinkingBlockIndex,
2952
- content: block.thinking,
2953
- partial: output
2954
- });
2955
- thinkingBlockIndex = -1;
2956
- }
2957
- if (thinkingParser) {
2958
- thinkingParser.processChunk(delta.content);
2959
- } else {
2960
- if (contentBlockIndex === -1) {
2961
- contentBlockIndex = output.content.length;
2962
- output.content.push({ type: "text", text: "" });
2963
- 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
+ });
2964
2952
  }
2965
- const block = output.content[contentBlockIndex];
2966
- block.text += delta.content;
2967
- stream.push({
2968
- type: "text_delta",
2969
- contentIndex: contentBlockIndex,
2970
- delta: delta.content,
2971
- partial: output
2972
- });
2973
2953
  }
2974
- }
2975
- if (delta.tool_calls && Array.isArray(delta.tool_calls)) {
2976
- for (const tc of delta.tool_calls) {
2977
- const idx = tc.index ?? 0;
2978
- if (!toolCallsState[idx]) {
2979
- toolCallsState[idx] = { arguments: "", id: "", name: "", contentIndex: 0 };
2980
- }
2981
- const state = toolCallsState[idx];
2982
- if (tc.id) state.id = tc.id;
2983
- if (tc.function?.name) state.name = tc.function.name;
2984
- if (state.emittedStart === void 0 && (state.id || state.name)) {
2985
- state.emittedStart = true;
2986
- state.contentIndex = output.content.length;
2987
- output.content.push({
2988
- type: "toolCall",
2989
- id: state.id,
2990
- name: state.name,
2991
- 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
2992
2962
  });
2993
- stream.push({ type: "toolcall_start", contentIndex: state.contentIndex, partial: output });
2994
- }
2995
- if (state.emittedStart) {
2996
- const block = output.content[state.contentIndex];
2997
- block.id = state.id;
2998
- block.name = state.name;
2963
+ thinkingBlockIndex = -1;
2999
2964
  }
3000
- if (tc.function?.arguments) {
3001
- const argDelta = tc.function.arguments;
3002
- 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;
3003
2975
  stream.push({
3004
- type: "toolcall_delta",
3005
- contentIndex: state.contentIndex,
3006
- delta: argDelta,
2976
+ type: "text_delta",
2977
+ contentIndex: contentBlockIndex,
2978
+ delta: delta.content,
3007
2979
  partial: output
3008
2980
  });
3009
2981
  }
3010
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;
3011
3023
  }
3012
3024
  }
3013
- if (choice.finish_reason) {
3014
- output.stopReason = choice.finish_reason;
3015
- }
3016
- }
3017
- } catch (e) {
3018
- if (e instanceof SyntaxError) {
3019
- if (process.env.QODER_DEBUG) {
3020
- 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;
3021
3031
  }
3022
- continue;
3032
+ throw e;
3023
3033
  }
3024
- throw e;
3025
3034
  }
3026
3035
  }
3027
- }
3028
- await reader.cancel().catch(() => {
3029
- });
3030
- if (thinkingParser) {
3031
- thinkingParser.finalize();
3032
- }
3033
- if (thinkingBlockIndex !== -1) {
3034
- const block = output.content[thinkingBlockIndex];
3035
- stream.push({
3036
- type: "thinking_end",
3037
- contentIndex: thinkingBlockIndex,
3038
- content: block.thinking,
3039
- partial: output
3036
+ await reader.cancel().catch(() => {
3040
3037
  });
3041
- }
3042
- for (const state of toolCallsState) {
3043
- if (state?.emittedStart && !state.emittedEnd) {
3044
- state.emittedEnd = true;
3045
- let args = {};
3046
- try {
3047
- args = JSON.parse(state.arguments || "{}");
3048
- } catch {
3049
- }
3050
- const block = output.content[state.contentIndex];
3051
- block.arguments = args;
3038
+ if (thinkingParser) {
3039
+ thinkingParser.finalize();
3040
+ }
3041
+ if (thinkingBlockIndex !== -1) {
3042
+ const block = output.content[thinkingBlockIndex];
3052
3043
  stream.push({
3053
- type: "toolcall_end",
3054
- contentIndex: state.contentIndex,
3055
- toolCall: {
3056
- type: "toolCall",
3057
- id: state.id,
3058
- name: state.name,
3059
- arguments: args
3060
- },
3044
+ type: "thinking_end",
3045
+ contentIndex: thinkingBlockIndex,
3046
+ content: block.thinking,
3061
3047
  partial: output
3062
3048
  });
3063
3049
  }
3064
- }
3065
- if (toolCallsState.some((state) => state?.emittedStart)) {
3066
- output.stopReason = "toolUse";
3067
- }
3068
- if (isDegenerateDsmlTurn(output, rawReasoningTail)) {
3069
- output.stopReason = "error";
3070
- output.errorMessage = "Qoder server error: degenerate model output (unparseable DSML tool-call markup, no tool call executed)";
3071
- 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
+ });
3072
3088
  stream.end();
3073
- 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);
3074
3102
  }
3075
- stream.push({
3076
- type: "done",
3077
- reason: output.stopReason,
3078
- message: output
3079
- });
3080
- stream.end();
3081
3103
  } catch (e) {
3082
3104
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
3083
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.8",
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",