pi-coralbricks-provider 1.0.6 → 1.0.7

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/README.md CHANGED
@@ -23,7 +23,6 @@ _A [pi](https://github.com/earendil-works/pi-coding-agent) provider extension fo
23
23
  - **Per-family thinking levels** — zai-style `thinking` control for GLM (including a *real* off switch), `reasoning_effort` for Kimi K3 and GPT-OSS
24
24
  - **Accurate cost tracking** — pricing mirrors Coral's published rates, and cached reads are **$0 on every model**, so pi's computed cost matches Coral's own `usage.cost` to the token
25
25
  - **Self-healing model sync** — stale-while-revalidate from the authenticated `/v1/models` (or the unauthenticated [public catalog](https://www.coralbricks.ai/api/public/models) before auth), hot-swapped at session start
26
- - **Streaming repair** — transparently fixes Coral's gpt-oss tool-call delta index fragmentation so streamed tool calls always accumulate correctly
27
26
  - **synbad-validated** — [synbad](https://github.com/synthetic-lab/synbad) tool-calling and reasoning-parsing evals pass 15/15 on GLM 5.2, GLM 5.3, and Kimi K3 in both unary and streaming modes
28
27
 
29
28
  ## Installation
@@ -152,16 +151,6 @@ Coral's gateway follows the OpenAI Chat Completions API:
152
151
  - **`supportsStrictMode: false`** — Kimi K3 (no strict JSON-schema tool definitions).
153
152
  - **`requiresReasoningContentOnAssistantMessages: true`** — Kimi K3.
154
153
 
155
- ### Streaming Tool-Call Repair
156
-
157
- On the raw wire, Coral occasionally emits a streamed tool call's final arguments fragment on a **new delta index** instead of continuing the existing one (`"index": 1` mid-call), which fragments the call under any spec-compliant accumulator. Observed on gpt-oss-120b; GLM and Kimi streams are correct.
158
-
159
- This extension's `streamSimple` pipes SSE responses through a repair stream that rewrites id-less, name-less tool-call deltas claiming a fresh index onto the last real call's index. Deltas that do carry an id/name (new calls, parallel calls) pass through untouched, and streams that are already correct are byte-identical. Verify against a live model:
160
-
161
- ```bash
162
- CORALBRICKS_API_KEY=cb_... bun run scripts/probe-stream-fix.ts [model-id]
163
- ```
164
-
165
154
  ### Patch Overrides & Custom Models
166
155
 
167
156
  - **`patch.json`** — per-model overrides applied on top of `models.json` (reasoning flags, pricing corrections, compat settings, thinking level maps). Currently empty — the curated defaults match the live API.
@@ -178,7 +167,7 @@ Validated with [synbad](https://github.com/synthetic-lab/synbad) — Synthetic's
178
167
  | GLM 5.2 FP4 | 15/15 ✅ | 15/15 ✅ | |
179
168
  | GLM 5.3 FP4 | 15/15 ✅ | 15/15 ✅ | |
180
169
  | Kimi K3 | 15/15 ✅ | 15/15 ✅ | |
181
- | GPT-OSS 120B | 14/15 ⚠️ | 10–11/15 ❌ raw | repaired in-extension (see above) |
170
+ | GPT-OSS 120B | 14/15 ⚠️ | pending re-eval | gateway tool-call index bug is fixed upstream |
182
171
 
183
172
  The remaining gpt-oss quirk is model-side: it answers "Paris and London" with a single batched tool call even when `parallel_tool_calls: true` — not a gateway bug.
184
173
 
package/index.ts CHANGED
@@ -424,84 +424,6 @@ async function revalidateModels(apiKey: string | undefined, embeddedModels: Json
424
424
  return merged;
425
425
  }
426
426
 
427
- // Streaming: Coral gateway tool-call index repair
428
- //
429
- // Some Coral models (observed on gpt-oss-120b) emit a streamed tool call's
430
- // final arguments fragment on a NEW delta index instead of continuing the
431
- // existing one:
432
- //
433
- // {"tool_calls":[{"id":"call_1","index":0,"function":{"name":"get_weather","arguments":"{\"location\": \"Par"}}]}
434
- // {"tool_calls":[{"index":1,"function":{"arguments":"is\"}"}}]} ← wrong index
435
- //
436
- // OpenAI semantics say a delta can only START a tool call when it carries an
437
- // id (and a function name); continuations omit them. pi-ai's accumulator
438
- // merges by index, so a mis-indexed fragment materializes a second, truncated
439
- // tool call. Per choice, we track the index of the last fragment that carried
440
- // id/name and rewrite id-less, name-less deltas that claim a different index
441
- // onto it. Deltas that DO carry id/name (new calls, parallel calls) pass
442
- // through untouched, as do Coral streams that are already correct (GLM/Kimi).
443
-
444
- function repairToolCallIndices(chunk: any, lastToolCallIndex: Map<number, number>): boolean {
445
- const choices = chunk?.choices;
446
- if (!Array.isArray(choices)) return false;
447
- let changed = false;
448
- for (const choice of choices) {
449
- const toolCalls = choice?.delta?.tool_calls;
450
- if (!Array.isArray(toolCalls) || toolCalls.length === 0) continue;
451
- const choiceIndex = typeof choice.index === "number" ? choice.index : 0;
452
- for (const tc of toolCalls) {
453
- const hasId = typeof tc?.id === "string" && tc.id.length > 0;
454
- const hasName = typeof tc?.function?.name === "string" && tc.function.name.length > 0;
455
- if (hasId || hasName) {
456
- lastToolCallIndex.set(choiceIndex, tc.index);
457
- continue;
458
- }
459
- const previous = lastToolCallIndex.get(choiceIndex);
460
- if (previous !== undefined && typeof tc?.index === "number" && tc.index !== previous) {
461
- tc.index = previous;
462
- changed = true;
463
- }
464
- }
465
- }
466
- return changed;
467
- }
468
-
469
- function createToolCallIndexFixer(): TransformStream<Uint8Array, Uint8Array> {
470
- const decoder = new TextDecoder();
471
- const encoder = new TextEncoder();
472
- const lastToolCallIndex = new Map<number, number>();
473
- let buffer = "";
474
-
475
- const fixLine = (line: string): string => {
476
- if (!line.startsWith("data:") || !line.includes("\"tool_calls\"")) return line;
477
- const payload = line.slice(5).trim();
478
- if (!payload || payload === "[DONE]") return line;
479
- let chunk: any;
480
- try {
481
- chunk = JSON.parse(payload);
482
- } catch {
483
- return line;
484
- }
485
- const changed = repairToolCallIndices(chunk, lastToolCallIndex);
486
- return changed ? "data: " + JSON.stringify(chunk) : line;
487
- };
488
-
489
- return new TransformStream<Uint8Array, Uint8Array>({
490
- transform(chunk, controller) {
491
- buffer += decoder.decode(chunk, { stream: true });
492
- let newline: number;
493
- while ((newline = buffer.indexOf("\n")) >= 0) {
494
- const line = buffer.slice(0, newline);
495
- buffer = buffer.slice(newline + 1);
496
- controller.enqueue(encoder.encode(fixLine(line) + "\n"));
497
- }
498
- },
499
- flush(controller) {
500
- if (buffer.length > 0) controller.enqueue(encoder.encode(fixLine(buffer)));
501
- },
502
- });
503
- }
504
-
505
427
  export function streamCoral(
506
428
  model: any,
507
429
  context: any,
@@ -522,24 +444,8 @@ export function streamCoral(
522
444
  const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
523
445
  const { reasoning: _reasoning, ...streamOptions } = options ?? {};
524
446
 
525
- // Per-request fetch interceptor (never a global patch — concurrent agent and
526
- // helper-model streams must not see each other's wrappers).
527
- const upstreamFetch = streamOptions.fetch ?? globalThis.fetch;
528
- const coralFetch: typeof globalThis.fetch = async (input, init) => {
529
- const response = await upstreamFetch(input as RequestInfo | URL, init);
530
- const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : (input as Request).url;
531
- const isEventStream = (response.headers.get("content-type") ?? "").includes("text/event-stream");
532
- if (!response.body || !isEventStream || !url.includes("/chat/completions")) return response;
533
- return new Response(response.body.pipeThrough(createToolCallIndexFixer()), {
534
- headers: response.headers,
535
- status: response.status,
536
- statusText: response.statusText,
537
- });
538
- };
539
-
540
447
  return streamOpenAICompletions(model, context, {
541
448
  ...streamOptions,
542
- fetch: coralFetch,
543
449
  reasoningEffort,
544
450
  apiKey,
545
451
  } as any);
@@ -596,4 +502,4 @@ export default function (pi: ExtensionAPI) {
596
502
  });
597
503
  }
598
504
 
599
- export { applyPatch, buildModels, mergeWithEmbedded, transformApiModel, transformCatalogModel, activeDeprecatedModels, withDeprecated, parseContextWindow, repairToolCallIndices, PROVIDER_ID, BASE_URL };
505
+ export { applyPatch, buildModels, mergeWithEmbedded, transformApiModel, transformCatalogModel, activeDeprecatedModels, withDeprecated, parseContextWindow, PROVIDER_ID, BASE_URL };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-coralbricks-provider",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "CoralBricks provider extension for pi - Access GLM 5.2/5.3, Kimi K3, and GPT-OSS 120B through the Coral Inference API",
5
5
  "author": "monotykamary",
6
6
  "homepage": "https://github.com/monotykamary/pi-coralbricks-provider#readme",
@@ -40,7 +40,6 @@
40
40
  "custom-models.json",
41
41
  "patch.json",
42
42
  "scripts/update-models.js",
43
- "scripts/probe-stream-fix.ts",
44
43
  "README.md",
45
44
  "LICENSE"
46
45
  ],
@@ -1,62 +0,0 @@
1
- /**
2
- * Live probe: stream gpt-oss-120b through streamCoral (the extension's
3
- * streamSimple path) and verify the tool-call index bug is repaired — the
4
- * accumulated message must contain exactly one complete tool call.
5
- */
6
- import { streamCoral } from "../index.ts";
7
- import modelsData from "../models.json" with { type: "json" };
8
-
9
- const apiKey = process.env.CORALBRICKS_API_KEY;
10
- if (!apiKey) throw new Error("CORALBRICKS_API_KEY not set");
11
-
12
- const modelId = process.argv[2] ?? "gpt-oss-120b";
13
- const model = {
14
- ...(modelsData as any[]).find((m) => m.id === modelId)!,
15
- provider: "coralbricks",
16
- baseUrl: "https://inference.coralbricks.ai/v1",
17
- api: "openai-completions",
18
- };
19
-
20
- const context = {
21
- systemPrompt: "You are a helpful assistant.",
22
- messages: [
23
- { role: "user", content: [{ type: "text", text: "What is the weather in Paris? Use the tool." }] },
24
- ],
25
- tools: [
26
- {
27
- name: "get_weather",
28
- description: "Get current weather for a location",
29
- parameters: {
30
- type: "object",
31
- properties: { location: { type: "string", description: "City name" } },
32
- required: ["location"],
33
- } as any,
34
- },
35
- ],
36
- };
37
-
38
- const events = streamCoral(model, context, { apiKey, reasoning: "high" } as any);
39
- let finalMessage: any = null;
40
- for await (const event of events) {
41
- if (event.type === "done") finalMessage = event.message;
42
- if (event.type === "error") finalMessage = event.error;
43
- }
44
-
45
- if (!finalMessage) throw new Error("no final message");
46
- if (finalMessage.stopReason === "error") {
47
- throw new Error("stream error: " + finalMessage.errorMessage);
48
- }
49
-
50
- const calls = (finalMessage.content ?? []).filter((c: any) => c.type === "toolCall");
51
- console.log("stopReason:", finalMessage.stopReason);
52
- console.log("toolCalls:", calls.length);
53
- for (const call of calls) {
54
- console.log(" -", call.name, JSON.stringify(call.arguments));
55
- }
56
-
57
- if (calls.length !== 1) throw new Error(`expected exactly 1 tool call, got ${calls.length}`);
58
- const args = typeof calls[0].arguments === "string" ? JSON.parse(calls[0].arguments) : calls[0].arguments;
59
- if (!args.location || !/paris/i.test(String(args.location))) {
60
- throw new Error("arguments malformed: " + JSON.stringify(calls[0].arguments));
61
- }
62
- console.log("PASS: streamCoral produced exactly one complete tool call");