pi-provider-qoder-cn 0.2.28 → 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 (2) hide show
  1. package/dist/index.js +133 -16
  2. package/package.json +1 -1
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
  }
@@ -3357,6 +3423,7 @@ function transformMessagesForQoder(messages) {
3357
3423
  // src/protocol/stream.ts
3358
3424
  var healCooldowns = /* @__PURE__ */ new Map();
3359
3425
  var HEAL_COOLDOWN_MS = 6e4;
3426
+ var CONTENT_CLASSIFY_BUFFER_LIMIT = 8 * 1024;
3360
3427
  function stableHash(prefix, ...inputs) {
3361
3428
  const hash = crypto3.createHash("sha256");
3362
3429
  hash.update(prefix);
@@ -3739,13 +3806,43 @@ function streamQoder(model, context, options) {
3739
3806
  const toolCallsState = [];
3740
3807
  const thinkingEnabled = options?.reasoning !== false && options?.reasoning !== "off";
3741
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
+ };
3742
3840
  if (!started) {
3743
3841
  started = true;
3744
3842
  } else {
3745
3843
  output.content.length = 0;
3746
3844
  }
3747
3845
  stream.push({ type: "start", partial: output });
3748
- let sawDone = false;
3749
3846
  while (!sawDone) {
3750
3847
  const { done, value } = await reader.read();
3751
3848
  if (done) break;
@@ -3796,9 +3893,11 @@ function streamQoder(model, context, options) {
3796
3893
  const choice = inner.choices[0];
3797
3894
  const delta = choice.delta;
3798
3895
  if (delta) {
3896
+ sawAnyDelta = true;
3799
3897
  if (delta.reasoning_content) {
3800
3898
  rawReasoningTail = (rawReasoningTail + delta.reasoning_content).slice(-512);
3801
3899
  const reasoningChunk = stripDsmlResidue(stripThinkingTags(delta.reasoning_content));
3900
+ if (reasoningChunk.trim().length > 0) reasoningChannelHadBody = true;
3802
3901
  if (reasoningChunk) {
3803
3902
  if (thinkingBlockIndex === -1) {
3804
3903
  thinkingBlockIndex = output.content.length;
@@ -3827,24 +3926,14 @@ function streamQoder(model, context, options) {
3827
3926
  thinkingBlockIndex = -1;
3828
3927
  }
3829
3928
  if (thinkingParser) {
3830
- thinkingParser.processChunk(delta.content);
3929
+ contentBuffer += delta.content;
3930
+ flushContentBuffer();
3831
3931
  } else {
3832
- if (contentBlockIndex === -1) {
3833
- contentBlockIndex = output.content.length;
3834
- output.content.push({ type: "text", text: "" });
3835
- stream.push({ type: "text_start", contentIndex: contentBlockIndex, partial: output });
3836
- }
3837
- const block = output.content[contentBlockIndex];
3838
- block.text += delta.content;
3839
- stream.push({
3840
- type: "text_delta",
3841
- contentIndex: contentBlockIndex,
3842
- delta: delta.content,
3843
- partial: output
3844
- });
3932
+ emitPlainText(delta.content);
3845
3933
  }
3846
3934
  }
3847
3935
  if (delta.tool_calls && Array.isArray(delta.tool_calls)) {
3936
+ flushContentBuffer(true);
3848
3937
  for (const tc of delta.tool_calls) {
3849
3938
  const idx = tc.index ?? 0;
3850
3939
  if (!toolCallsState[idx]) {
@@ -3883,6 +3972,7 @@ function streamQoder(model, context, options) {
3883
3972
  }
3884
3973
  }
3885
3974
  if (choice.finish_reason) {
3975
+ sawFinishReason = true;
3886
3976
  output.stopReason = choice.finish_reason;
3887
3977
  }
3888
3978
  }
@@ -3899,6 +3989,7 @@ function streamQoder(model, context, options) {
3899
3989
  }
3900
3990
  await reader.cancel().catch(() => {
3901
3991
  });
3992
+ flushContentBuffer(true);
3902
3993
  if (thinkingParser) {
3903
3994
  thinkingParser.finalize();
3904
3995
  }
@@ -3944,6 +4035,32 @@ function streamQoder(model, context, options) {
3944
4035
  stream.end();
3945
4036
  return;
3946
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
+ }
3947
4064
  stream.push({
3948
4065
  type: "done",
3949
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.28",
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",