pi-free 2.2.4 → 2.2.6

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 (35) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +1 -1
  3. package/banner.svg +1 -1
  4. package/config.ts +58 -29
  5. package/constants.ts +1 -4
  6. package/index.ts +30 -24
  7. package/lib/built-in-toggle.ts +185 -18
  8. package/lib/provider-cache.ts +22 -0
  9. package/package.json +74 -74
  10. package/provider-failover/benchmark-lookup.ts +4 -1
  11. package/provider-failover/benchmarks-chunk-0.ts +570 -570
  12. package/provider-failover/benchmarks-chunk-1.ts +676 -676
  13. package/provider-failover/benchmarks-chunk-2.ts +673 -673
  14. package/provider-failover/benchmarks-chunk-3.ts +680 -680
  15. package/provider-failover/benchmarks-chunk-4.ts +683 -683
  16. package/provider-failover/benchmarks-chunk-5.ts +816 -474
  17. package/provider-helper.ts +64 -0
  18. package/providers/bai/bai.ts +8 -2
  19. package/providers/crofai/crofai.ts +8 -2
  20. package/providers/deepinfra/deepinfra.ts +8 -2
  21. package/providers/dynamic-built-in/index.ts +36 -26
  22. package/providers/kilo/kilo.ts +41 -22
  23. package/providers/novita/novita.ts +8 -2
  24. package/providers/openmodel/openmodel.ts +8 -2
  25. package/providers/qoder/models.ts +63 -175
  26. package/providers/qoder/qoder.ts +49 -84
  27. package/providers/qoder/stream.ts +182 -274
  28. package/providers/qoder/transform.ts +5 -2
  29. package/providers/routeway/routeway.ts +8 -2
  30. package/providers/sambanova/sambanova.ts +12 -6
  31. package/providers/together/together.ts +8 -2
  32. package/providers/tokenrouter/tokenrouter.ts +8 -2
  33. package/providers/zenmux/zenmux.ts +8 -2
  34. package/providers/codestral/codestral.ts +0 -128
  35. package/providers/qoder/encoding.ts +0 -48
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * Qoder custom streaming handler.
3
3
  *
4
- * Qoder's API is NOT OpenAI-compatible it uses a proprietary protocol at
5
- * `api3.qoder.sh/algo/api/v2/service/pro/sse/agent_chat_generation` with
6
- * COSY-signed headers, WAF-encoded bodies, and a custom SSE event format.
4
+ * Qoder's current production API is OpenAI-compatible and lives at
5
+ * `api2-v2.qoder.sh/model/v1/chat/completions` with standard Bearer auth.
6
+ * The legacy proprietary SSE/protocol endpoint at api3.qoder.sh has been
7
+ * decommissioned and returns 500 Internal Server Error.
7
8
  *
8
- * This module implements the full `streamSimple` interface that Pi expects,
9
- * bridging Qoder's proprietary streaming to Pi's `AssistantMessageEventStream`.
9
+ * This module implements the `streamSimple` interface that Pi expects,
10
+ * translating Qoder's response format to Pi's `AssistantMessageEventStream`.
10
11
  */
11
12
 
12
- import crypto from "node:crypto";
13
13
  import type {
14
14
  Api,
15
15
  AssistantMessage,
@@ -17,19 +17,21 @@ import type {
17
17
  Context,
18
18
  Model,
19
19
  SimpleStreamOptions,
20
+ StopReason,
20
21
  TextContent,
21
22
  ThinkingContent,
22
23
  ToolCall,
23
24
  } from "@earendil-works/pi-ai";
24
25
  import * as PiAi from "@earendil-works/pi-ai";
25
- import { buildAuthHeaders, getMachineId } from "./cosy.ts";
26
- import { getCachedModelConfig } from "./models.ts";
27
- import { getCachedCredentials } from "./auth.ts";
28
- import { qoderEncodeBody } from "./encoding.ts";
26
+ import { BASE_URL_QODER } from "../../constants.ts";
27
+ import { createLogger } from "../../lib/logger.ts";
28
+ import { getCachedModelConfig, staticModels } from "./models.ts";
29
29
  import { ThinkingTagParser } from "./thinking-parser.ts";
30
30
  import { transformMessagesForQoder, transformTools } from "./transform.ts";
31
31
 
32
- // ─── Helpers ─────────────────────────────────────────────────────────────────
32
+ // =============================================================================
33
+ // Helpers / State
34
+ // =============================================================================
33
35
 
34
36
  interface ToolCallState {
35
37
  arguments: string;
@@ -47,52 +49,12 @@ interface StreamState {
47
49
  thinkingBlockIndex: number;
48
50
  toolCallsState: ToolCallState[];
49
51
  thinkingParser: ThinkingTagParser | null;
52
+ hasReasoningContent: boolean;
50
53
  }
51
54
 
52
- function stableHash(prefix: string, ...inputs: string[]): string {
53
- const hash = crypto.createHash("sha256");
54
- hash.update(prefix);
55
- for (const input of inputs) {
56
- hash.update("\0");
57
- hash.update(input);
58
- }
59
- return hash.digest("hex").slice(0, 16);
60
- }
61
-
62
- function stableChatRecordID(
63
- model: string,
64
- messages: Array<{ role?: string; content?: unknown }>,
65
- tools: unknown,
66
- maxTokens: number,
67
- ): string {
68
- const hash = crypto.createHash("sha256");
69
- hash.update("qoder-record");
70
- hash.update("\0");
71
- hash.update(model);
72
- for (const msg of messages) {
73
- if (msg?.role) {
74
- hash.update("\0");
75
- hash.update(msg.role);
76
- }
77
- if (msg?.content) {
78
- hash.update("\0");
79
- hash.update(
80
- typeof msg.content === "string"
81
- ? msg.content
82
- : JSON.stringify(msg.content),
83
- );
84
- }
85
- }
86
- if (tools) {
87
- hash.update("\0");
88
- hash.update(JSON.stringify(tools));
89
- }
90
- hash.update("\0");
91
- hash.update(`mt=${maxTokens}`);
92
- return hash.digest("hex").slice(0, 16);
93
- }
94
-
95
- // ─── Delta processing helpers ────────────────────────────────────────────────
55
+ // =============================================================================
56
+ // Delta processing
57
+ // =============================================================================
96
58
 
97
59
  function processReasoningDelta(
98
60
  state: StreamState,
@@ -134,7 +96,7 @@ function closeThinkingBlock(state: StreamState): void {
134
96
  }
135
97
 
136
98
  function processTextDelta(state: StreamState, text: string): void {
137
- if (state.thinkingParser) {
99
+ if (state.thinkingParser && !state.hasReasoningContent) {
138
100
  state.thinkingParser.processChunk(text);
139
101
  return;
140
102
  }
@@ -210,8 +172,9 @@ function processDelta(
210
172
  state: StreamState,
211
173
  delta: Record<string, unknown>,
212
174
  ): void {
213
- // 1. Reasoning content (API-native)
175
+ // 1. Reasoning content (API-native OpenAI-compatible extension)
214
176
  if (delta.reasoning_content) {
177
+ state.hasReasoningContent = true;
215
178
  processReasoningDelta(state, delta.reasoning_content as string);
216
179
  }
217
180
 
@@ -256,40 +219,42 @@ function finalizeToolCalls(state: StreamState): void {
256
219
  }
257
220
  }
258
221
 
259
- // ─── SSE parsing ─────────────────────────────────────────────────────────────
222
+ // =============================================================================
223
+ // SSE parsing (OpenAI-compatible stream)
224
+ // =============================================================================
260
225
 
261
- function handleSSELine(
262
- state: StreamState,
263
- line: string,
264
- ): boolean {
226
+ function handleSSELine(state: StreamState, line: string): boolean {
265
227
  if (!line.startsWith("data:")) return false;
266
228
 
267
229
  const dataStr = line.slice(5).trim();
268
230
  if (dataStr === "[DONE]") return true;
269
231
 
232
+ let parsed: Record<string, unknown>;
270
233
  try {
271
- const envelope = JSON.parse(dataStr);
272
- if (envelope.statusCodeValue && envelope.statusCodeValue !== 200) {
273
- throw new Error(
274
- `Upstream status ${envelope.statusCodeValue}: ${envelope.body}`,
275
- );
276
- }
234
+ parsed = JSON.parse(dataStr);
235
+ } catch {
236
+ // Skip unparseable SSE lines
237
+ return false;
238
+ }
277
239
 
278
- const innerStr = envelope.body;
279
- if (!innerStr || innerStr === "[DONE]") return false;
240
+ if (parsed.error) {
241
+ throw new Error(
242
+ `Qoder SSE error: ${(parsed.error as { message?: string }).message || JSON.stringify(parsed.error)}`,
243
+ );
244
+ }
280
245
 
281
- const inner = JSON.parse(innerStr);
282
- if (inner.choices && inner.choices.length > 0) {
283
- const choice = inner.choices[0];
284
- if (choice.delta) {
285
- processDelta(state, choice.delta);
286
- }
287
- if (choice.finish_reason) {
288
- state.output.stopReason = choice.finish_reason;
289
- }
246
+ if (
247
+ parsed.choices &&
248
+ Array.isArray(parsed.choices) &&
249
+ parsed.choices.length > 0
250
+ ) {
251
+ const choice = parsed.choices[0] as Record<string, unknown>;
252
+ if (choice.delta) {
253
+ processDelta(state, choice.delta as Record<string, unknown>);
254
+ }
255
+ if (choice.finish_reason) {
256
+ state.output.stopReason = choice.finish_reason as StopReason;
290
257
  }
291
- } catch {
292
- // Skip unparseable SSE lines
293
258
  }
294
259
  return false;
295
260
  }
@@ -307,6 +272,12 @@ async function consumeSSEStream(
307
272
 
308
273
  buffer += decoder.decode(value, { stream: true });
309
274
 
275
+ if (buffer.length > MAX_SSE_BUFFER_BYTES) {
276
+ throw new Error(
277
+ `Qoder SSE buffer exceeded ${MAX_SSE_BUFFER_BYTES} bytes without a newline; stream appears malformed.`,
278
+ );
279
+ }
280
+
310
281
  while (true) {
311
282
  const lineEnd = buffer.indexOf("\n");
312
283
  if (lineEnd === -1) break;
@@ -320,115 +291,146 @@ async function consumeSSEStream(
320
291
  }
321
292
  }
322
293
 
323
- // ─── Request builder ─────────────────────────────────────────────────────────
294
+ // =============================================================================
295
+ // Request builder (OpenAI-compatible)
296
+ // =============================================================================
324
297
 
325
- async function fetchQoderStream(
326
- setup: StreamSetup,
327
- signal?: AbortSignal,
328
- ): Promise<ReadableStream<Uint8Array>> {
329
- const {
298
+ const QODER_CHAT_URL = `${BASE_URL_QODER}/model/v1/chat/completions`;
299
+
300
+ const logger = createLogger("qoder");
301
+
302
+ /** Max SSE line buffer size before we treat the stream as malformed. */
303
+ const MAX_SSE_BUFFER_BYTES = 1024 * 1024; // 1 MB
304
+
305
+ /** Redact a bearer token so it never leaks into logs or error messages. */
306
+ function redactToken(token: string | undefined): string {
307
+ if (!token) return "(none)";
308
+ if (token.length <= 8) return "***";
309
+ return `${token.slice(0, 3)}...${token.slice(-3)}`;
310
+ }
311
+
312
+ interface StreamSetup {
313
+ accessToken: string;
314
+ qoderModel: string;
315
+ modelConfig: Record<string, unknown>;
316
+ normalizedMessages: unknown[];
317
+ systemText: string;
318
+ maxTokens: number;
319
+ toolsRaw: unknown;
320
+ }
321
+
322
+ function buildStreamSetup(
323
+ model: Model<Api>,
324
+ context: Context,
325
+ options: SimpleStreamOptions | undefined,
326
+ ): StreamSetup {
327
+ const accessToken = options?.apiKey;
328
+ if (!accessToken) {
329
+ throw new Error(
330
+ "Qoder credentials not set. Run /login qoder or set QODER_PERSONAL_ACCESS_TOKEN.",
331
+ );
332
+ }
333
+
334
+ // Log for diagnostics (visible in pi output)
335
+ const isDebug = process.env.QODER_DEBUG === "1";
336
+
337
+ const qoderModel = model.id;
338
+ const modelConfig = getCachedModelConfig(qoderModel) || {
339
+ key: qoderModel,
340
+ is_reasoning: isReasoningModel(qoderModel),
341
+ max_output_tokens: 32768,
342
+ source: "system",
343
+ };
344
+
345
+ const maxOutputTokens = (modelConfig.max_output_tokens as number) || 32768;
346
+
347
+ let normalizedMessages = transformMessagesForQoder(context.messages);
348
+ const systemText = context.systemPrompt || "";
349
+
350
+ // Prepend system prompt as a system message if present.
351
+ if (systemText) {
352
+ normalizedMessages = [
353
+ { role: "system", content: systemText },
354
+ ...normalizedMessages,
355
+ ];
356
+ }
357
+
358
+ const maxTokens = resolveMaxTokens(maxOutputTokens, options?.maxTokens);
359
+
360
+ const toolsRaw =
361
+ context.tools && context.tools.length > 0
362
+ ? transformTools(context.tools)
363
+ : undefined;
364
+
365
+ if (isDebug) {
366
+ logger.info("[QODER] streaming request", {
367
+ endpoint: QODER_CHAT_URL,
368
+ model: qoderModel,
369
+ messages: normalizedMessages.length,
370
+ token: redactToken(accessToken),
371
+ });
372
+ }
373
+
374
+ return {
330
375
  accessToken,
331
376
  qoderModel,
332
377
  modelConfig,
333
378
  normalizedMessages,
334
- lastUserText,
335
379
  systemText,
336
380
  maxTokens,
337
381
  toolsRaw,
338
- recordID,
339
- userID,
340
- name,
341
- email,
342
- machineID,
343
- } = setup;
344
- const sessionID = stableHash("qoder-session", userID, qoderModel);
382
+ };
383
+ }
345
384
 
346
- const isReasoning = Boolean(modelConfig.is_reasoning);
385
+ async function fetchQoderStream(
386
+ setup: StreamSetup,
387
+ signal?: AbortSignal,
388
+ ): Promise<ReadableStream<Uint8Array>> {
389
+ const { accessToken, qoderModel, normalizedMessages, maxTokens, toolsRaw } =
390
+ setup;
347
391
 
348
392
  const reqBody: Record<string, unknown> = {
349
- request_id: crypto.randomUUID(),
350
- request_set_id: recordID,
351
- chat_record_id: recordID,
352
- session_id: sessionID,
353
- stream: true,
354
- chat_task: "FREE_INPUT",
355
- is_reply: true,
356
- is_retry: false,
357
- source: 1,
358
- version: "3",
359
- session_type: "qodercli",
360
- agent_id: "agent_common",
361
- task_id: "common",
362
- code_language: "",
363
- chat_prompt: "",
364
- image_urls: null,
365
- aliyun_user_type: "",
366
- system: systemText,
393
+ model: qoderModel,
367
394
  messages: normalizedMessages,
368
- tools: toolsRaw || [],
369
- parameters: { max_tokens: maxTokens },
370
- chat_context: {
371
- chatPrompt: "",
372
- imageUrls: null,
373
- extra: {
374
- context: [],
375
- modelConfig: {
376
- key: qoderModel,
377
- is_reasoning: isReasoning,
378
- },
379
- originalContent: lastUserText,
380
- },
381
- features: [],
382
- text: lastUserText,
383
- },
384
- model_config: modelConfig,
385
- business: {
386
- product: "cli",
387
- version: "1.0.0",
388
- type: "agent",
389
- stage: "start",
390
- id: crypto.randomUUID(),
391
- name: lastUserText.substring(0, 30),
392
- begin_at: Date.now(),
393
- },
395
+ stream: true,
394
396
  };
395
397
 
396
- const bodyBytes = Buffer.from(JSON.stringify(reqBody));
397
- const encodedBody = qoderEncodeBody(bodyBytes);
398
- const encodedBytes = Buffer.from(encodedBody, "utf8");
399
-
400
- const chatURL =
401
- "https://api3.qoder.sh/algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1";
398
+ if (toolsRaw && Array.isArray(toolsRaw) && toolsRaw.length > 0) {
399
+ reqBody.tools = toolsRaw;
400
+ }
402
401
 
403
- const headers = buildAuthHeaders(encodedBytes, chatURL, {
404
- userID,
405
- authToken: accessToken,
406
- name,
407
- email,
408
- machineID,
409
- });
402
+ if (maxTokens > 0) {
403
+ reqBody.max_tokens = maxTokens;
404
+ }
410
405
 
411
- const modelSource = modelConfig.source || "system";
406
+ const headers: Record<string, string> = {
407
+ "Content-Type": "application/json",
408
+ Accept: "text/event-stream",
409
+ Authorization: `Bearer ${accessToken}`,
410
+ "User-Agent": "pi-free-providers",
411
+ };
412
412
 
413
- const response = await fetch(chatURL, {
413
+ const response = await fetch(QODER_CHAT_URL, {
414
414
  method: "POST",
415
- headers: {
416
- "Content-Type": "application/json",
417
- Accept: "text/event-stream",
418
- "Cache-Control": "no-cache",
419
- "Accept-Encoding": "identity",
420
- "X-Model-Key": qoderModel,
421
- "X-Model-Source": modelSource as string,
422
- ...headers,
423
- },
424
- body: encodedBytes,
415
+ headers,
416
+ body: Buffer.from(JSON.stringify(reqBody)),
425
417
  signal,
426
418
  });
427
419
 
428
420
  if (!response.ok) {
429
421
  const errText = await response.text();
422
+ const truncated =
423
+ errText.length > 500 ? `${errText.slice(0, 500)}...` : errText;
424
+ logger.error("[QODER] API request failed", {
425
+ status: response.status,
426
+ statusText: response.statusText,
427
+ model: qoderModel,
428
+ responseLength: errText.length,
429
+ response: truncated,
430
+ token: redactToken(accessToken),
431
+ });
430
432
  throw new Error(
431
- `Qoder API request failed: ${response.status} ${response.statusText}. Response: ${errText}`,
433
+ `Qoder API request failed: ${response.status} ${response.statusText} at ${QODER_CHAT_URL}. Model: ${qoderModel}.`,
432
434
  );
433
435
  }
434
436
 
@@ -437,12 +439,10 @@ async function fetchQoderStream(
437
439
  return body;
438
440
  }
439
441
 
440
- // ─── Stream handler ──────────────────────────────────────────────────────────
442
+ // =============================================================================
443
+ // Stream handler (Pi `streamSimple` implementation)
444
+ // =============================================================================
441
445
 
442
- /**
443
- * Main streaming handler for Qoder API requests.
444
- * This is passed as the `streamSimple` option in `pi.registerProvider`.
445
- */
446
446
  export function streamQoder(
447
447
  model: Model<Api>,
448
448
  context: Context,
@@ -485,85 +485,6 @@ export function streamQoder(
485
485
  return stream;
486
486
  }
487
487
 
488
- interface StreamSetup {
489
- accessToken: string;
490
- qoderModel: string;
491
- modelConfig: Record<string, unknown>;
492
- normalizedMessages: unknown[];
493
- lastUserText: string;
494
- systemText: string;
495
- maxTokens: number;
496
- toolsRaw: unknown;
497
- recordID: string;
498
- userID: string;
499
- name: string;
500
- email: string;
501
- machineID: string;
502
- }
503
-
504
- function buildStreamSetup(
505
- model: Model<Api>,
506
- context: Context,
507
- options: SimpleStreamOptions | undefined,
508
- ): StreamSetup {
509
- const accessToken = options?.apiKey;
510
- if (!accessToken) {
511
- throw new Error(
512
- "Qoder credentials not set. Run /login qoder or set QODER_PERSONAL_ACCESS_TOKEN.",
513
- );
514
- }
515
-
516
- const cachedCreds = getCachedCredentials();
517
- const userID = cachedCreds?.userID || "qoder-user";
518
- const name = cachedCreds?.name || "Qoder User";
519
- const email = cachedCreds?.email || "user@qoder.com";
520
- const machineID = cachedCreds?.machineID || getMachineId();
521
-
522
- const qoderModel = model.id;
523
- const modelConfig = getCachedModelConfig(qoderModel) || {
524
- key: qoderModel,
525
- is_reasoning: isReasoningModel(qoderModel),
526
- max_output_tokens: 32768,
527
- source: "system",
528
- };
529
- modelConfig.key = qoderModel;
530
-
531
- const maxOutputTokens = modelConfig.max_output_tokens || 32768;
532
-
533
- const normalizedMessages = transformMessagesForQoder(context.messages);
534
- const systemText = context.systemPrompt || "";
535
- const lastUserText = extractLastUserText(normalizedMessages);
536
-
537
- const maxTokens = resolveMaxTokens(maxOutputTokens, options?.maxTokens);
538
-
539
- const toolsRaw =
540
- context.tools && context.tools.length > 0
541
- ? transformTools(context.tools)
542
- : undefined;
543
- const recordID = stableChatRecordID(
544
- qoderModel,
545
- normalizedMessages,
546
- toolsRaw,
547
- maxTokens,
548
- );
549
-
550
- return {
551
- accessToken,
552
- qoderModel,
553
- modelConfig,
554
- normalizedMessages,
555
- lastUserText,
556
- systemText,
557
- maxTokens,
558
- toolsRaw,
559
- recordID,
560
- userID,
561
- name,
562
- email,
563
- machineID,
564
- };
565
- }
566
-
567
488
  async function runStream(
568
489
  output: AssistantMessage,
569
490
  stream: AssistantMessageEventStream,
@@ -586,12 +507,13 @@ async function runStream(
586
507
  thinkingBlockIndex: -1,
587
508
  toolCallsState: [],
588
509
  thinkingParser,
510
+ hasReasoningContent: false,
589
511
  };
590
512
 
591
513
  stream.push({ type: "start", partial: output });
592
514
 
593
- const reader = await fetchQoderStream(setup, options?.signal).then(
594
- (s) => s.getReader(),
515
+ const reader = await fetchQoderStream(setup, options?.signal).then((s) =>
516
+ s.getReader(),
595
517
  );
596
518
 
597
519
  await consumeSSEStream(state, reader);
@@ -635,30 +557,16 @@ async function runStream(
635
557
  }
636
558
  }
637
559
 
638
- // ─── Small pure helpers ──────────────────────────────────────────────────────
560
+ // =============================================================================
561
+ // Small pure helpers
562
+ // =============================================================================
639
563
 
640
- function isReasoningModel(modelId: string): boolean {
641
- return (
642
- modelId === "ultimate" ||
643
- modelId === "performance" ||
644
- modelId.includes("dmodel") ||
645
- modelId.includes("dfmodel")
646
- );
647
- }
564
+ const REASONING_MODEL_IDS = new Set(
565
+ staticModels.filter((m) => m.reasoning).map((m) => m.id),
566
+ );
648
567
 
649
- function extractLastUserText(
650
- messages: Array<{ role?: string; content?: unknown }>,
651
- ): string {
652
- for (let i = messages.length - 1; i >= 0; i--) {
653
- const msg = messages[i];
654
- if (msg?.role !== "user") continue;
655
- const content = msg.content;
656
- if (typeof content === "string") return content;
657
- if (Array.isArray(content)) {
658
- return content.map((c) => ("text" in c ? c.text : "")).join("");
659
- }
660
- }
661
- return "";
568
+ function isReasoningModel(modelId: string): boolean {
569
+ return REASONING_MODEL_IDS.has(modelId);
662
570
  }
663
571
 
664
572
  function resolveMaxTokens(maxOutputTokens: number, requested?: number): number {
@@ -41,7 +41,7 @@ type QoderContent = string | Array<QoderTextPart | QoderImagePart>;
41
41
 
42
42
  /** OpenAI-style message sent to the Qoder API. */
43
43
  interface QoderMessage {
44
- role: "user" | "assistant" | "tool";
44
+ role: "user" | "assistant" | "tool" | "system";
45
45
  content: QoderContent | null;
46
46
  tool_calls?: QoderToolCall[];
47
47
  tool_call_id?: string;
@@ -150,7 +150,10 @@ function transformAssistantMessage(am: AssistantMessage): QoderMessage {
150
150
  if (block.type === "text") {
151
151
  content += (block as TextContent).text;
152
152
  } else if (block.type === "thinking") {
153
- content += `<thinking>${(block as ThinkingContent).thinking}</thinking>\n\n`;
153
+ // Skip sending previous thinking blocks for the OpenAI-compatible API.
154
+ // Qoder current API produces reasoning_content natively during streaming;
155
+ // re-emitting prior reasoning as <thinking> tags is unnecessary and
156
+ // can confuse newer model versions.
154
157
  } else if (block.type === "toolCall") {
155
158
  const tc = block as ToolCall;
156
159
  toolCalls.push({
@@ -45,7 +45,11 @@ import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
45
45
  import { wrapSessionStartHandler } from "../../lib/session-start-metrics.ts";
46
46
  import { cleanModelName, fetchWithRetry } from "../../lib/util.ts";
47
47
  import { fetchWithTimeout } from "../../lib/util.ts";
48
- import { createReRegister, setupProvider } from "../../provider-helper.ts";
48
+ import {
49
+ createReRegister,
50
+ loadCachedOrFetchModels,
51
+ setupProvider,
52
+ } from "../../provider-helper.ts";
49
53
 
50
54
  const _logger = createLogger("routeway");
51
55
 
@@ -303,7 +307,9 @@ export default async function routewayProvider(pi: ExtensionAPI) {
303
307
  return;
304
308
  }
305
309
 
306
- const allModels = await fetchRoutewayModels(apiKey);
310
+ const allModels = await loadCachedOrFetchModels(PROVIDER_ROUTEWAY, () =>
311
+ fetchRoutewayModels(apiKey),
312
+ );
307
313
 
308
314
  if (allModels.length === 0) {
309
315
  _logger.warn("[routeway] No chat models available");
@@ -38,7 +38,11 @@ import {
38
38
  fetchOpenAICompatibleModels,
39
39
  fetchWithTimeout,
40
40
  } from "../../lib/util.ts";
41
- import { createReRegister, setupProvider } from "../../provider-helper.ts";
41
+ import {
42
+ createReRegister,
43
+ loadCachedOrFetchModels,
44
+ setupProvider,
45
+ } from "../../provider-helper.ts";
42
46
 
43
47
  const _logger = createLogger("sambanova");
44
48
 
@@ -57,11 +61,13 @@ export default async function sambanovaProvider(pi: ExtensionAPI) {
57
61
  }
58
62
 
59
63
  // Fetch models via shared OpenAI-compatible helper
60
- const allModels = await fetchOpenAICompatibleModels(
61
- "sambanova",
62
- BASE_URL_SAMBANOVA,
63
- apiKey,
64
- { maxTokens: 8_192 },
64
+ const allModels = await loadCachedOrFetchModels(PROVIDER_SAMBANOVA, () =>
65
+ fetchOpenAICompatibleModels(
66
+ "sambanova",
67
+ BASE_URL_SAMBANOVA,
68
+ apiKey,
69
+ { maxTokens: 8_192 },
70
+ ),
65
71
  );
66
72
 
67
73
  if (allModels.length === 0) {
@@ -50,7 +50,11 @@ import { createProviderProbe } from "../../lib/provider-probe.ts";
50
50
  import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
51
51
  import { wrapSessionStartHandler } from "../../lib/session-start-metrics.ts";
52
52
  import { fetchWithRetry, fetchWithTimeout } from "../../lib/util.ts";
53
- import { createReRegister, setupProvider } from "../../provider-helper.ts";
53
+ import {
54
+ createReRegister,
55
+ loadCachedOrFetchModels,
56
+ setupProvider,
57
+ } from "../../provider-helper.ts";
54
58
 
55
59
  const _logger = createLogger("together");
56
60
 
@@ -150,7 +154,9 @@ export default async function togetherProvider(pi: ExtensionAPI) {
150
154
  }
151
155
 
152
156
  // Fetch models
153
- const allModels = await fetchTogetherModels(apiKey);
157
+ const allModels = await loadCachedOrFetchModels(PROVIDER_TOGETHER, () =>
158
+ fetchTogetherModels(apiKey),
159
+ );
154
160
 
155
161
  if (allModels.length === 0) {
156
162
  _logger.warn("[together] No chat models available");