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 +1 -12
- package/index.ts +1 -95
- package/package.json +1 -2
- package/scripts/probe-stream-fix.ts +0 -62
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 ⚠️ |
|
|
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,
|
|
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.
|
|
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");
|