thincoder 0.12.15 → 0.12.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.12.15",
3
+ "version": "0.12.17",
4
4
  "description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
5
5
  "keywords": [
6
6
  "ai",
package/src/config.mjs CHANGED
@@ -93,6 +93,8 @@ const MODEL_SPECS = [
93
93
  ["deepseek-chat", { context: 256_000, maxOutput: 384_000, thinking: false, prefixMode: true, cacheMode: "prompt", thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["high", "max"], tempRange: [0, 2] }],
94
94
  // Kimi series
95
95
  ["kimi-k3", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, cacheMode: "auto", thinkApi: "effort", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"] }],
96
+ // Qwen router prefixes model IDs with provider namespace: kimi/kimi-k3 → kimi-k3 (IK7K4V)
97
+ ["kimi/kimi-k3", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, cacheMode: "auto", thinkApi: "effort", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"] }],
96
98
  // Kimi For Coding endpoint uses the short model ID "k3" (same specs as kimi-k3) — IK5VGJ
97
99
  ["k3", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, cacheMode: "auto", thinkApi: "effort", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"] }],
98
100
  ["kimi-k2", { context: 256_000, maxOutput: 128_000, thinking: false, partialMode: true, multimodal: true, cacheMode: "none" }],
@@ -3,23 +3,49 @@
3
3
  * Extracted from core.mjs. Parses Server-Sent Events for LLM chat responses.
4
4
  */
5
5
  export async function readSSE(response, { onToken, onReasoning, rules, signal, firedPatterns: sharedFired }) {
6
- // Early intercept: non-SSE responses are error bodies, not streams.
7
- // Read as text to preserve the full error detail (status + body).
6
+ // Early intercept: non-SSE responses either error bodies (HTTP >= 400) or
7
+ // valid single-chunk JSON completions (some APIs return JSON despite stream:true).
8
8
  const contentType = response.headers.get("content-type") || ""
9
9
  if (!contentType.includes("event-stream")) {
10
10
  const body = await response.text().catch(() => "")
11
- let errorMsg = ""
11
+ if (response.status >= 400) {
12
+ let errorMsg = ""
13
+ try {
14
+ const parsed = JSON.parse(body)
15
+ errorMsg = parsed?.error?.message
16
+ || parsed?.base_resp?.status_msg
17
+ || parsed?.detail
18
+ || parsed?.message
19
+ || parsed?.msg
20
+ || (typeof parsed.error === "string" ? parsed.error : "")
21
+ } catch { /* not JSON */ }
22
+ if (!errorMsg) errorMsg = body.slice(0, 500)
23
+ throw new Error(`API error: HTTP ${response.status} — ${errorMsg}`)
24
+ }
25
+ // HTTP < 400 non-SSE: might be a valid single-chunk JSON response (e.g. proxy
26
+ // stripped SSE framing). Try to parse as a chat.completion.chunk.
12
27
  try {
13
28
  const parsed = JSON.parse(body)
14
- errorMsg = parsed?.error?.message
15
- || parsed?.base_resp?.status_msg
16
- || parsed?.detail
17
- || parsed?.message
18
- || parsed?.msg
19
- || (typeof parsed.error === "string" ? parsed.error : "")
20
- } catch { /* not JSON */ }
21
- if (!errorMsg) errorMsg = body.slice(0, 500) // fallback: show raw body preview
22
- throw new Error(`API error: HTTP ${response.status} ${errorMsg}`)
29
+ const choice = parsed.choices?.[0]
30
+ if (choice) {
31
+ const result = { content: "", reasoning: "", toolCalls: [], usage: parsed.usage ?? null, finishReason: null }
32
+ const delta = choice.delta ?? {}
33
+ result.content = delta.content ?? ""
34
+ result.reasoning = delta.reasoning_content ?? ""
35
+ result.finishReason = choice.finish_reason ?? null
36
+ for (const tc of delta.tool_calls ?? []) {
37
+ const slot = (result.toolCalls[tc.index ?? result.toolCalls.length] ??= { id: "", name: "", arguments: "" })
38
+ if (tc.id) slot.id = tc.id
39
+ if (tc.function?.name && !slot.name) slot.name = tc.function.name
40
+ if (tc.function?.arguments) slot.arguments += tc.function.arguments
41
+ }
42
+ if (result.content) onToken?.(result.content)
43
+ if (result.reasoning) onReasoning?.(result.reasoning)
44
+ return result
45
+ }
46
+ } catch { /* not parseable JSON */ }
47
+ // Not an error response but not a valid chunk either — unexpected
48
+ throw new Error(`API error: HTTP ${response.status} — unexpected non-SSE response: ${body.slice(0, 200)}`)
23
49
  }
24
50
 
25
51
  const result = { content: "", reasoning: "", toolCalls: [], usage: null, finishReason: null }