pi-provider-qoder-cn 0.2.27 → 0.2.29

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 +10 -0
  2. package/dist/index.js +142 -17
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -14,6 +14,16 @@ load order (last wins), so keep this package listed last or uninstall upstream.
14
14
  pi install npm:pi-provider-qoder-cn
15
15
  ```
16
16
 
17
+ If your npm registry is a mirror (for example `registry.npmmirror.com`), it may
18
+ lag behind the official registry by hours. A release that shows as green on
19
+ GitHub but is missing from `npm view` is usually this, not a failed publish.
20
+ Check the official source directly and install with an explicit registry:
21
+
22
+ ```bash
23
+ npm view pi-provider-qoder-cn version --registry=https://registry.npmjs.org/
24
+ npm install pi-provider-qoder-cn@latest --registry=https://registry.npmjs.org/
25
+ ```
26
+
17
27
  ## What this fork adds
18
28
 
19
29
  | Feature | Upstream | This fork |
package/dist/index.js CHANGED
@@ -3008,6 +3008,21 @@ var THINKING_TAG_VARIANTS = [
3008
3008
  { open: "<thought>", close: "</thought>" }
3009
3009
  ];
3010
3010
  var ALL_THINKING_TAGS = THINKING_TAG_VARIANTS.flatMap((variant) => [variant.open, variant.close]);
3011
+ function hasAnyCompleteThinkingTag(text) {
3012
+ return THINKING_TAG_VARIANTS.some((variant) => text.includes(variant.open) || text.includes(variant.close));
3013
+ }
3014
+ function hasOrphanThinkingCloser(text) {
3015
+ for (const variant of THINKING_TAG_VARIANTS) {
3016
+ const closePos = text.indexOf(variant.close);
3017
+ if (closePos === -1) continue;
3018
+ const openerBefore = THINKING_TAG_VARIANTS.some((other) => {
3019
+ const openPos = text.indexOf(other.open);
3020
+ return openPos !== -1 && openPos < closePos;
3021
+ });
3022
+ if (!openerBefore) return true;
3023
+ }
3024
+ return false;
3025
+ }
3011
3026
  function getTrailingPossibleTagPrefixLength(text, tag) {
3012
3027
  const maxPrefixLength = Math.min(text.length, tag.length - 1);
3013
3028
  for (let len = maxPrefixLength; len > 0; len--) {
@@ -3055,6 +3070,23 @@ var ThinkingTagParser = class {
3055
3070
  textBlockIndex = null;
3056
3071
  lastTextBlockIndex = null;
3057
3072
  activeEndTag = THINKING_TAG_VARIANTS[0].close;
3073
+ orphanCloserMeansThinking = false;
3074
+ /**
3075
+ * Must be called before the first `processChunk`.
3076
+ *
3077
+ * When the reasoning_content channel carried no thinking body this turn
3078
+ * (only a bare opener tag, or nothing at all), an orphan closer in the
3079
+ * content stream is preceded by THINKING text rather than answer text.
3080
+ * stream.ts decides this once, at the first content delta, because Qoder
3081
+ * sends the reasoning channel before the content channel.
3082
+ *
3083
+ * Default false preserves the historical behaviour: the opener and its
3084
+ * thinking text arrived via reasoning_content, so text before the orphan
3085
+ * closer in the content stream is visible answer text.
3086
+ */
3087
+ setOrphanCloserMeansThinking(value) {
3088
+ this.orphanCloserMeansThinking = value;
3089
+ }
3058
3090
  processChunk(chunk) {
3059
3091
  this.textBuffer += chunk;
3060
3092
  while (this.textBuffer.length > 0) {
@@ -3091,6 +3123,17 @@ var ThinkingTagParser = class {
3091
3123
  content: block.thinking,
3092
3124
  partial: this.output
3093
3125
  });
3126
+ } else if (this.orphanCloserMeansThinking && !this.thinkingExtracted) {
3127
+ this.emitThinking(this.textBuffer);
3128
+ if (this.thinkingBlockIndex !== null) {
3129
+ const block = this.output.content[this.thinkingBlockIndex];
3130
+ this.stream.push({
3131
+ type: "thinking_end",
3132
+ contentIndex: this.thinkingBlockIndex,
3133
+ content: block.thinking,
3134
+ partial: this.output
3135
+ });
3136
+ }
3094
3137
  } else {
3095
3138
  this.emitText(this.textBuffer);
3096
3139
  }
@@ -3124,6 +3167,27 @@ var ThinkingTagParser = class {
3124
3167
  return;
3125
3168
  }
3126
3169
  if (bestCloseVariant !== null) {
3170
+ if (this.orphanCloserMeansThinking && bestClosePos > 0) {
3171
+ const thinkingText = this.textBuffer.slice(0, bestClosePos);
3172
+ this.emitThinking(thinkingText);
3173
+ if (this.thinkingBlockIndex !== null) {
3174
+ const block = this.output.content[this.thinkingBlockIndex];
3175
+ this.stream.push({
3176
+ type: "thinking_end",
3177
+ contentIndex: this.thinkingBlockIndex,
3178
+ content: block.thinking,
3179
+ partial: this.output
3180
+ });
3181
+ this.thinkingBlockIndex = null;
3182
+ }
3183
+ this.textBuffer = this.textBuffer.slice(bestClosePos + bestCloseVariant.close.length);
3184
+ if (this.textBuffer.startsWith("\n\n")) this.textBuffer = this.textBuffer.slice(2);
3185
+ else if (this.textBuffer.startsWith("\n")) this.textBuffer = this.textBuffer.slice(1);
3186
+ this.thinkingExtracted = true;
3187
+ this.lastTextBlockIndex = this.textBlockIndex;
3188
+ this.textBlockIndex = null;
3189
+ return;
3190
+ }
3127
3191
  if (bestClosePos > 0) this.emitText(this.textBuffer.slice(0, bestClosePos));
3128
3192
  this.textBuffer = this.textBuffer.slice(bestClosePos + bestCloseVariant.close.length);
3129
3193
  if (this.textBuffer.startsWith("\n\n")) this.textBuffer = this.textBuffer.slice(2);
@@ -3133,7 +3197,9 @@ var ThinkingTagParser = class {
3133
3197
  const trailingPrefixLength = getMaxTrailingPossibleTagPrefixLength(this.textBuffer, ALL_THINKING_TAGS);
3134
3198
  const safeLen = this.textBuffer.length - trailingPrefixLength;
3135
3199
  if (safeLen > 0) {
3136
- this.emitText(this.textBuffer.slice(0, safeLen));
3200
+ const safeText = this.textBuffer.slice(0, safeLen);
3201
+ if (this.orphanCloserMeansThinking) this.emitThinking(safeText);
3202
+ else this.emitText(safeText);
3137
3203
  this.textBuffer = this.textBuffer.slice(safeLen);
3138
3204
  }
3139
3205
  }
@@ -3233,7 +3299,14 @@ function transformTools(tools) {
3233
3299
  function transformMessagesForQoder(messages) {
3234
3300
  const normalizedMessages = [];
3235
3301
  const droppedToolCallIds = /* @__PURE__ */ new Set();
3302
+ const pendingToolImages = [];
3303
+ const flushToolImages = () => {
3304
+ if (pendingToolImages.length === 0) return;
3305
+ normalizedMessages.push(...pendingToolImages);
3306
+ pendingToolImages.length = 0;
3307
+ };
3236
3308
  for (const msg of messages) {
3309
+ if (msg.role !== "toolResult") flushToolImages();
3237
3310
  if (msg.role === "assistant" && (msg.stopReason === "error" || msg.stopReason === "aborted")) {
3238
3311
  const am = msg;
3239
3312
  if (Array.isArray(am.content)) {
@@ -3325,7 +3398,7 @@ function transformMessagesForQoder(messages) {
3325
3398
  });
3326
3399
  const images = getContentImages(tr);
3327
3400
  if (images.length > 0) {
3328
- normalizedMessages.push({
3401
+ pendingToolImages.push({
3329
3402
  role: "user",
3330
3403
  content: [
3331
3404
  {
@@ -3343,12 +3416,14 @@ function transformMessagesForQoder(messages) {
3343
3416
  }
3344
3417
  }
3345
3418
  }
3419
+ flushToolImages();
3346
3420
  return normalizedMessages;
3347
3421
  }
3348
3422
 
3349
3423
  // src/protocol/stream.ts
3350
3424
  var healCooldowns = /* @__PURE__ */ new Map();
3351
3425
  var HEAL_COOLDOWN_MS = 6e4;
3426
+ var CONTENT_CLASSIFY_BUFFER_LIMIT = 8 * 1024;
3352
3427
  function stableHash(prefix, ...inputs) {
3353
3428
  const hash = crypto3.createHash("sha256");
3354
3429
  hash.update(prefix);
@@ -3731,13 +3806,43 @@ function streamQoder(model, context, options) {
3731
3806
  const toolCallsState = [];
3732
3807
  const thinkingEnabled = options?.reasoning !== false && options?.reasoning !== "off";
3733
3808
  const thinkingParser = thinkingEnabled ? new ThinkingTagParser(output, stream) : null;
3809
+ let sawDone = false;
3810
+ let sawFinishReason = false;
3811
+ let sawAnyDelta = false;
3812
+ let reasoningChannelHadBody = false;
3813
+ let contentClassificationDone = false;
3814
+ let contentBuffer = "";
3815
+ const flushContentBuffer = (final = false) => {
3816
+ if (!contentBuffer) return;
3817
+ if (!contentClassificationDone && (final || hasAnyCompleteThinkingTag(contentBuffer) || contentBuffer.length >= CONTENT_CLASSIFY_BUFFER_LIMIT)) {
3818
+ if (thinkingParser && !reasoningChannelHadBody && hasOrphanThinkingCloser(contentBuffer)) {
3819
+ thinkingParser.setOrphanCloserMeansThinking(true);
3820
+ }
3821
+ contentClassificationDone = true;
3822
+ }
3823
+ if (!contentClassificationDone) return;
3824
+ const pending = contentBuffer;
3825
+ contentBuffer = "";
3826
+ if (thinkingParser) thinkingParser.processChunk(pending);
3827
+ else emitPlainText(pending);
3828
+ };
3829
+ const emitPlainText = (text) => {
3830
+ if (!text) return;
3831
+ if (contentBlockIndex === -1) {
3832
+ contentBlockIndex = output.content.length;
3833
+ output.content.push({ type: "text", text: "" });
3834
+ stream.push({ type: "text_start", contentIndex: contentBlockIndex, partial: output });
3835
+ }
3836
+ const block = output.content[contentBlockIndex];
3837
+ block.text += text;
3838
+ stream.push({ type: "text_delta", contentIndex: contentBlockIndex, delta: text, partial: output });
3839
+ };
3734
3840
  if (!started) {
3735
3841
  started = true;
3736
3842
  } else {
3737
3843
  output.content.length = 0;
3738
3844
  }
3739
3845
  stream.push({ type: "start", partial: output });
3740
- let sawDone = false;
3741
3846
  while (!sawDone) {
3742
3847
  const { done, value } = await reader.read();
3743
3848
  if (done) break;
@@ -3788,9 +3893,11 @@ function streamQoder(model, context, options) {
3788
3893
  const choice = inner.choices[0];
3789
3894
  const delta = choice.delta;
3790
3895
  if (delta) {
3896
+ sawAnyDelta = true;
3791
3897
  if (delta.reasoning_content) {
3792
3898
  rawReasoningTail = (rawReasoningTail + delta.reasoning_content).slice(-512);
3793
3899
  const reasoningChunk = stripDsmlResidue(stripThinkingTags(delta.reasoning_content));
3900
+ if (reasoningChunk.trim().length > 0) reasoningChannelHadBody = true;
3794
3901
  if (reasoningChunk) {
3795
3902
  if (thinkingBlockIndex === -1) {
3796
3903
  thinkingBlockIndex = output.content.length;
@@ -3819,24 +3926,14 @@ function streamQoder(model, context, options) {
3819
3926
  thinkingBlockIndex = -1;
3820
3927
  }
3821
3928
  if (thinkingParser) {
3822
- thinkingParser.processChunk(delta.content);
3929
+ contentBuffer += delta.content;
3930
+ flushContentBuffer();
3823
3931
  } else {
3824
- if (contentBlockIndex === -1) {
3825
- contentBlockIndex = output.content.length;
3826
- output.content.push({ type: "text", text: "" });
3827
- stream.push({ type: "text_start", contentIndex: contentBlockIndex, partial: output });
3828
- }
3829
- const block = output.content[contentBlockIndex];
3830
- block.text += delta.content;
3831
- stream.push({
3832
- type: "text_delta",
3833
- contentIndex: contentBlockIndex,
3834
- delta: delta.content,
3835
- partial: output
3836
- });
3932
+ emitPlainText(delta.content);
3837
3933
  }
3838
3934
  }
3839
3935
  if (delta.tool_calls && Array.isArray(delta.tool_calls)) {
3936
+ flushContentBuffer(true);
3840
3937
  for (const tc of delta.tool_calls) {
3841
3938
  const idx = tc.index ?? 0;
3842
3939
  if (!toolCallsState[idx]) {
@@ -3875,6 +3972,7 @@ function streamQoder(model, context, options) {
3875
3972
  }
3876
3973
  }
3877
3974
  if (choice.finish_reason) {
3975
+ sawFinishReason = true;
3878
3976
  output.stopReason = choice.finish_reason;
3879
3977
  }
3880
3978
  }
@@ -3891,6 +3989,7 @@ function streamQoder(model, context, options) {
3891
3989
  }
3892
3990
  await reader.cancel().catch(() => {
3893
3991
  });
3992
+ flushContentBuffer(true);
3894
3993
  if (thinkingParser) {
3895
3994
  thinkingParser.finalize();
3896
3995
  }
@@ -3936,6 +4035,32 @@ function streamQoder(model, context, options) {
3936
4035
  stream.end();
3937
4036
  return;
3938
4037
  }
4038
+ if (!sawAnyDelta) {
4039
+ if (!options?.signal?.aborted) {
4040
+ output.stopReason = "error";
4041
+ output.errorMessage = "Qoder server error: incomplete stream (no content deltas received)";
4042
+ stream.push({ type: "error", reason: "error", error: output });
4043
+ stream.end();
4044
+ return;
4045
+ }
4046
+ output.stopReason = "aborted";
4047
+ stream.push({ type: "error", reason: "aborted", error: output });
4048
+ stream.end();
4049
+ return;
4050
+ }
4051
+ if (!sawDone && !sawFinishReason) {
4052
+ if (!options?.signal?.aborted) {
4053
+ output.stopReason = "error";
4054
+ output.errorMessage = "Qoder server error: incomplete stream (response ended without [DONE] or finish_reason)";
4055
+ stream.push({ type: "error", reason: "error", error: output });
4056
+ stream.end();
4057
+ return;
4058
+ }
4059
+ output.stopReason = "aborted";
4060
+ stream.push({ type: "error", reason: "aborted", error: output });
4061
+ stream.end();
4062
+ return;
4063
+ }
3939
4064
  stream.push({
3940
4065
  type: "done",
3941
4066
  reason: output.stopReason,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-provider-qoder-cn",
3
- "version": "0.2.27",
3
+ "version": "0.2.29",
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",