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.
- package/CHANGELOG.md +24 -0
- package/README.md +1 -1
- package/banner.svg +1 -1
- package/config.ts +58 -29
- package/constants.ts +1 -4
- package/index.ts +30 -24
- package/lib/built-in-toggle.ts +185 -18
- package/lib/provider-cache.ts +22 -0
- package/package.json +74 -74
- package/provider-failover/benchmark-lookup.ts +4 -1
- package/provider-failover/benchmarks-chunk-0.ts +570 -570
- package/provider-failover/benchmarks-chunk-1.ts +676 -676
- package/provider-failover/benchmarks-chunk-2.ts +673 -673
- package/provider-failover/benchmarks-chunk-3.ts +680 -680
- package/provider-failover/benchmarks-chunk-4.ts +683 -683
- package/provider-failover/benchmarks-chunk-5.ts +816 -474
- package/provider-helper.ts +64 -0
- package/providers/bai/bai.ts +8 -2
- package/providers/crofai/crofai.ts +8 -2
- package/providers/deepinfra/deepinfra.ts +8 -2
- package/providers/dynamic-built-in/index.ts +36 -26
- package/providers/kilo/kilo.ts +41 -22
- package/providers/novita/novita.ts +8 -2
- package/providers/openmodel/openmodel.ts +8 -2
- package/providers/qoder/models.ts +63 -175
- package/providers/qoder/qoder.ts +49 -84
- package/providers/qoder/stream.ts +182 -274
- package/providers/qoder/transform.ts +5 -2
- package/providers/routeway/routeway.ts +8 -2
- package/providers/sambanova/sambanova.ts +12 -6
- package/providers/together/together.ts +8 -2
- package/providers/tokenrouter/tokenrouter.ts +8 -2
- package/providers/zenmux/zenmux.ts +8 -2
- package/providers/codestral/codestral.ts +0 -128
- 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
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
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
|
|
9
|
-
*
|
|
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 {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
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
|
-
//
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
234
|
+
parsed = JSON.parse(dataStr);
|
|
235
|
+
} catch {
|
|
236
|
+
// Skip unparseable SSE lines
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
277
239
|
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
//
|
|
294
|
+
// =============================================================================
|
|
295
|
+
// Request builder (OpenAI-compatible)
|
|
296
|
+
// =============================================================================
|
|
324
297
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
name,
|
|
341
|
-
email,
|
|
342
|
-
machineID,
|
|
343
|
-
} = setup;
|
|
344
|
-
const sessionID = stableHash("qoder-session", userID, qoderModel);
|
|
382
|
+
};
|
|
383
|
+
}
|
|
345
384
|
|
|
346
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
name,
|
|
407
|
-
email,
|
|
408
|
-
machineID,
|
|
409
|
-
});
|
|
402
|
+
if (maxTokens > 0) {
|
|
403
|
+
reqBody.max_tokens = maxTokens;
|
|
404
|
+
}
|
|
410
405
|
|
|
411
|
-
const
|
|
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(
|
|
413
|
+
const response = await fetch(QODER_CHAT_URL, {
|
|
414
414
|
method: "POST",
|
|
415
|
-
headers
|
|
416
|
-
|
|
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}.
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
560
|
+
// =============================================================================
|
|
561
|
+
// Small pure helpers
|
|
562
|
+
// =============================================================================
|
|
639
563
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
|
650
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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 {
|
|
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
|
|
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");
|