@pedrofariasx/qwenproxy 1.1.0
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/LICENSE +13 -0
- package/README.md +292 -0
- package/bin/qwenproxy.mjs +11 -0
- package/package.json +56 -0
- package/src/api/models.ts +183 -0
- package/src/api/server.ts +126 -0
- package/src/cache/memory-cache.ts +186 -0
- package/src/core/account-manager.ts +132 -0
- package/src/core/accounts.ts +78 -0
- package/src/core/config.ts +91 -0
- package/src/core/database.ts +92 -0
- package/src/core/logger.ts +96 -0
- package/src/core/metrics.ts +169 -0
- package/src/core/model-registry.ts +30 -0
- package/src/core/stream-registry.ts +40 -0
- package/src/core/watchdog.ts +130 -0
- package/src/index.ts +7 -0
- package/src/linter/extraction-engine.ts +165 -0
- package/src/linter/index.ts +258 -0
- package/src/linter/repair-normalize.ts +245 -0
- package/src/linter/safety-gate.ts +219 -0
- package/src/linter/streaming-state-machine.ts +252 -0
- package/src/linter/structural-parser.ts +352 -0
- package/src/linter/types.ts +74 -0
- package/src/login.ts +228 -0
- package/src/routes/chat.ts +801 -0
- package/src/routes/upload.ts +700 -0
- package/src/services/playwright.ts +778 -0
- package/src/services/qwen.ts +500 -0
- package/src/tests/advanced.test.ts +227 -0
- package/src/tests/agenticStress.test.ts +360 -0
- package/src/tests/concurrency.test.ts +103 -0
- package/src/tests/concurrentChat.test.ts +71 -0
- package/src/tests/delta.test.ts +63 -0
- package/src/tests/index.test.ts +356 -0
- package/src/tests/jsonFix.test.ts +98 -0
- package/src/tests/linter.test.ts +151 -0
- package/src/tests/parallel.test.ts +42 -0
- package/src/tests/parser.test.ts +89 -0
- package/src/tests/rotation.test.ts +45 -0
- package/src/tests/streamingOptimizations.test.ts +328 -0
- package/src/tests/structureVerification.test.ts +176 -0
- package/src/tools/ast.ts +15 -0
- package/src/tools/coercion.ts +67 -0
- package/src/tools/confidence.ts +48 -0
- package/src/tools/detector.ts +40 -0
- package/src/tools/executor.ts +236 -0
- package/src/tools/parser.ts +446 -0
- package/src/tools/pipeline.ts +122 -0
- package/src/tools/registry-runtime.ts +34 -0
- package/src/tools/registry.ts +142 -0
- package/src/tools/repair.ts +42 -0
- package/src/tools/schema.ts +285 -0
- package/src/tools/types.ts +104 -0
- package/src/tools/validator.ts +33 -0
- package/src/utils/context-truncation.ts +61 -0
- package/src/utils/json.ts +114 -0
- package/src/utils/qwen-stream-parser.ts +286 -0
- package/src/utils/types.ts +101 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* File: json.ts
|
|
3
|
+
* Project: qwenproxy
|
|
4
|
+
* Robust JSON parsing utilities
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
function sanitizeAndBalance(input: string): { result: string; openBraces: number; openBrackets: number } {
|
|
8
|
+
let out = '';
|
|
9
|
+
let openBraces = 0;
|
|
10
|
+
let openBrackets = 0;
|
|
11
|
+
let inString = false;
|
|
12
|
+
let escaped = false;
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < input.length; i++) {
|
|
15
|
+
const char = input[i];
|
|
16
|
+
if (escaped) {
|
|
17
|
+
const validEscapes = ['n', 'r', 't', 'u', '"', '\\', '/'];
|
|
18
|
+
if (validEscapes.includes(char)) {
|
|
19
|
+
if (char === 'u') {
|
|
20
|
+
const next4 = input.substring(i + 1, i + 5);
|
|
21
|
+
out += /^[0-9a-fA-F]{4}$/.test(next4) ? '\\' + char : '\\\\' + char;
|
|
22
|
+
} else if (['n', 'r', 't'].includes(char)) {
|
|
23
|
+
const isWinPath = /[a-zA-Z]:\\/i.test(input) || /[a-zA-Z]:\//i.test(input);
|
|
24
|
+
const nextChar = input[i + 1] || '';
|
|
25
|
+
out += (isWinPath && /^[a-zA-Z0-9]/.test(nextChar)) ? '\\\\' + char : '\\' + char;
|
|
26
|
+
} else {
|
|
27
|
+
out += '\\' + char;
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
out += '\\\\' + char;
|
|
31
|
+
}
|
|
32
|
+
escaped = false;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (char === '\\') { escaped = true; continue; }
|
|
36
|
+
if (char === '"') { inString = !inString; out += char; continue; }
|
|
37
|
+
if (inString) {
|
|
38
|
+
if (char === '\n') out += '\\n';
|
|
39
|
+
else if (char === '\r') out += '\\r';
|
|
40
|
+
else if (char === '\t') out += '\\t';
|
|
41
|
+
else if (char.charCodeAt(0) < 32) out += '\\u' + char.charCodeAt(0).toString(16).padStart(4, '0');
|
|
42
|
+
else out += char;
|
|
43
|
+
} else {
|
|
44
|
+
out += char;
|
|
45
|
+
if (char === '{') openBraces++;
|
|
46
|
+
if (char === '}') openBraces--;
|
|
47
|
+
if (char === '[') openBrackets++;
|
|
48
|
+
if (char === ']') openBrackets--;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { result: out, openBraces, openBrackets };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function closeBraces(input: string, openBraces: number, openBrackets: number): string {
|
|
55
|
+
let out = input;
|
|
56
|
+
if (openBrackets > 0) out += ']'.repeat(openBrackets);
|
|
57
|
+
if (openBraces > 0) out += '}'.repeat(openBraces);
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function robustParseJSON(str: string): any {
|
|
62
|
+
let sanitized = str.trim();
|
|
63
|
+
sanitized = sanitized.replace(/^```json\s*/, '').replace(/```$/, '').trim();
|
|
64
|
+
|
|
65
|
+
const firstBrace = sanitized.indexOf('{');
|
|
66
|
+
if (firstBrace === -1) return null;
|
|
67
|
+
|
|
68
|
+
let jsonPart = sanitized.substring(firstBrace);
|
|
69
|
+
try { return JSON.parse(jsonPart); } catch (e) { /* continue */ }
|
|
70
|
+
|
|
71
|
+
let currentJson = jsonPart.replace(/([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)(\s*:)/g, '$1"$2"$3');
|
|
72
|
+
currentJson = currentJson.replace(/([{,]\s*)"([a-zA-Z0-9_]+)"\s*:\s*"\2"\s*:/g, '$1"$2":');
|
|
73
|
+
currentJson = currentJson.replace(/([{,]\s*)([a-zA-Z0-9_]+)\s*:\s*\2\s*:/g, '$1$2:');
|
|
74
|
+
|
|
75
|
+
try { return JSON.parse(currentJson); } catch (e) { /* continue */ }
|
|
76
|
+
|
|
77
|
+
let cleaned = currentJson.trim();
|
|
78
|
+
while (cleaned.length > 0 && !/[}\]"0-9a-z]/i.test(cleaned[cleaned.length - 1])) {
|
|
79
|
+
cleaned = cleaned.slice(0, -1).trim();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const { result: fixedJson, openBraces, openBrackets } = sanitizeAndBalance(cleaned);
|
|
83
|
+
let lastBalancedIndex = -1;
|
|
84
|
+
|
|
85
|
+
{ let ob = 0, bk = 0, ins = false, esc = false;
|
|
86
|
+
for (let i = 0; i < fixedJson.length; i++) {
|
|
87
|
+
const c = fixedJson[i];
|
|
88
|
+
if (esc) { esc = false; continue; }
|
|
89
|
+
if (c === '\\') { esc = true; continue; }
|
|
90
|
+
if (c === '"') { ins = !ins; continue; }
|
|
91
|
+
if (!ins) {
|
|
92
|
+
if (c === '{') ob++; if (c === '}') ob--;
|
|
93
|
+
if (c === '[') bk++; if (c === ']') bk--;
|
|
94
|
+
if (ob === 0 && bk === 0) lastBalancedIndex = i;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let tempJson = fixedJson;
|
|
100
|
+
if (lastBalancedIndex !== -1 && (openBraces !== 0 || openBrackets !== 0 || fixedJson.length > lastBalancedIndex + 1)) {
|
|
101
|
+
tempJson = fixedJson.substring(0, lastBalancedIndex + 1);
|
|
102
|
+
} else if (openBraces > 0 || openBrackets > 0) {
|
|
103
|
+
tempJson = closeBraces(fixedJson, openBraces, openBrackets);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try { return JSON.parse(tempJson); } catch (e) {
|
|
107
|
+
let aggressive = fixedJson.trim();
|
|
108
|
+
if (aggressive.endsWith(',')) aggressive = aggressive.slice(0, -1);
|
|
109
|
+
const { result: aggFixed, openBraces: ob, openBrackets: bk } = sanitizeAndBalance(aggressive);
|
|
110
|
+
try { return JSON.parse(closeBraces(aggFixed, ob, bk)); } catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* File: qwen-stream-parser.ts
|
|
3
|
+
* Project: qwenproxy
|
|
4
|
+
* Author: Pedro Farias
|
|
5
|
+
* Created: 2026-06-02
|
|
6
|
+
*
|
|
7
|
+
* Shared SSE parser for Qwen stream responses.
|
|
8
|
+
* Eliminates duplication between streaming and non-streaming code paths.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { updateSessionParent } from '../services/qwen.js';
|
|
12
|
+
import { getIncrementalDelta } from '../routes/chat.js';
|
|
13
|
+
import { StreamingToolParser } from '../tools/parser.js';
|
|
14
|
+
import type { FunctionToolDefinition } from '../tools/types.js';
|
|
15
|
+
|
|
16
|
+
export interface QwenStreamDelta {
|
|
17
|
+
phase: string;
|
|
18
|
+
content?: string;
|
|
19
|
+
extra?: {
|
|
20
|
+
summary_thought?: {
|
|
21
|
+
content: string[];
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface QwenStreamChunk {
|
|
27
|
+
'response.created'?: {
|
|
28
|
+
response_id: string;
|
|
29
|
+
};
|
|
30
|
+
response_id?: string;
|
|
31
|
+
usage?: {
|
|
32
|
+
input_tokens?: number;
|
|
33
|
+
output_tokens?: number;
|
|
34
|
+
};
|
|
35
|
+
choices?: Array<{
|
|
36
|
+
delta: QwenStreamDelta;
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ParsedChunkResult {
|
|
41
|
+
/** The extracted content string (delta for thinking or answer phase). Empty if none. */
|
|
42
|
+
content: string;
|
|
43
|
+
/** True if this chunk belongs to the thinking/reasoning phase. */
|
|
44
|
+
isThinking: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface StreamParserState {
|
|
48
|
+
targetResponseId: string | null;
|
|
49
|
+
currentThoughtIndex: number;
|
|
50
|
+
lastFullContent: string;
|
|
51
|
+
reasoningBuffer: string;
|
|
52
|
+
promptTokens: number;
|
|
53
|
+
completionTokens: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface QwenStreamParseOptions {
|
|
57
|
+
/** Tool definitions for the streaming tool parser. Pass [] or null to disable tool parsing. */
|
|
58
|
+
tools?: FunctionToolDefinition[];
|
|
59
|
+
/** Callback invoked when a response_id is discovered. */
|
|
60
|
+
onTargetResponseId?: (responseId: string, uiSessionId: string) => void;
|
|
61
|
+
/** Callback invoked for each parsed thinking content delta. */
|
|
62
|
+
onThinking?: (content: string) => void;
|
|
63
|
+
/** Callback invoked for each parsed answer content delta. */
|
|
64
|
+
onAnswer?: (content: string) => void;
|
|
65
|
+
/** Callback invoked for each tool call parsed from the answer stream. */
|
|
66
|
+
onToolCall?: (toolCall: {
|
|
67
|
+
id: string;
|
|
68
|
+
name: string;
|
|
69
|
+
arguments: Record<string, unknown>;
|
|
70
|
+
}) => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* QwenStreamParser handles parsing of Qwen's SSE stream chunks for both
|
|
75
|
+
* streaming and non-streaming response modes.
|
|
76
|
+
*
|
|
77
|
+
* It extracts:
|
|
78
|
+
* - response_id for session tracking
|
|
79
|
+
* - usage statistics (input/output tokens)
|
|
80
|
+
* - thinking_summary content (reasoning)
|
|
81
|
+
* - answer content (final response text)
|
|
82
|
+
* - tool calls embedded in the answer stream
|
|
83
|
+
*/
|
|
84
|
+
export class QwenStreamParser {
|
|
85
|
+
private readonly uiSessionId: string;
|
|
86
|
+
private readonly options: Required<Pick<QwenStreamParseOptions, 'tools'>> & Omit<QwenStreamParseOptions, 'tools'>;
|
|
87
|
+
|
|
88
|
+
private _state: StreamParserState;
|
|
89
|
+
private toolParser: StreamingToolParser | null;
|
|
90
|
+
private readonly bufferAccumulator: string[] = [];
|
|
91
|
+
|
|
92
|
+
constructor(uiSessionId: string, options: QwenStreamParseOptions = {}) {
|
|
93
|
+
this.uiSessionId = uiSessionId;
|
|
94
|
+
this.options = {
|
|
95
|
+
tools: options.tools ?? [],
|
|
96
|
+
onTargetResponseId: options.onTargetResponseId,
|
|
97
|
+
onThinking: options.onThinking,
|
|
98
|
+
onAnswer: options.onAnswer,
|
|
99
|
+
onToolCall: options.onToolCall,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
this._state = {
|
|
103
|
+
targetResponseId: null,
|
|
104
|
+
currentThoughtIndex: 0,
|
|
105
|
+
lastFullContent: '',
|
|
106
|
+
reasoningBuffer: '',
|
|
107
|
+
promptTokens: 0,
|
|
108
|
+
completionTokens: 0,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
this.toolParser = this.options.tools && this.options.tools.length > 0
|
|
112
|
+
? new StreamingToolParser(this.options.tools)
|
|
113
|
+
: null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Get the current parser state (read-only). */
|
|
117
|
+
get state(): Readonly<StreamParserState> {
|
|
118
|
+
return this._state;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Get accumulated reasoning buffer. */
|
|
122
|
+
get reasoningBuffer(): string {
|
|
123
|
+
return this._state.reasoningBuffer;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Get accumulated answer content. */
|
|
127
|
+
get answerContent(): string {
|
|
128
|
+
return this._state.lastFullContent;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Get token usage statistics. */
|
|
132
|
+
get usage(): { promptTokens: number; completionTokens: number } {
|
|
133
|
+
return {
|
|
134
|
+
promptTokens: this._state.promptTokens,
|
|
135
|
+
completionTokens: this._state.completionTokens,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Process a single raw SSE line (the part after "data: ").
|
|
141
|
+
* Returns the parsed result, or null if the line should be skipped.
|
|
142
|
+
*/
|
|
143
|
+
parseLine(rawData: string): ParsedChunkResult | null {
|
|
144
|
+
if (rawData === '[DONE]') return null;
|
|
145
|
+
|
|
146
|
+
let chunk: QwenStreamChunk;
|
|
147
|
+
try {
|
|
148
|
+
chunk = JSON.parse(rawData);
|
|
149
|
+
} catch {
|
|
150
|
+
return null; // Partial/malformed chunk, skip
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Track response_id for session continuity
|
|
154
|
+
this.updateResponseId(chunk);
|
|
155
|
+
|
|
156
|
+
// Track token usage
|
|
157
|
+
this.updateUsage(chunk);
|
|
158
|
+
|
|
159
|
+
// Extract content delta
|
|
160
|
+
const delta = this.extractDelta(chunk);
|
|
161
|
+
if (!delta) return null;
|
|
162
|
+
|
|
163
|
+
if (delta.content === 'FINISHED') return null;
|
|
164
|
+
|
|
165
|
+
if (delta.isThinking) {
|
|
166
|
+
this._state.reasoningBuffer += delta.content;
|
|
167
|
+
this.options.onThinking?.(delta.content);
|
|
168
|
+
} else {
|
|
169
|
+
// Update incremental content tracking
|
|
170
|
+
const deltaResult = getIncrementalDelta(this._state.lastFullContent, delta.content);
|
|
171
|
+
this._state.lastFullContent = deltaResult.matchedContent;
|
|
172
|
+
|
|
173
|
+
// Process through tool parser if enabled
|
|
174
|
+
if (this.toolParser) {
|
|
175
|
+
const { text, toolCalls } = this.toolParser.feed(delta.content);
|
|
176
|
+
// text is the lead-in before any tool_call tag.
|
|
177
|
+
// In non-streaming mode, the lead-in is preserved and recovered only if tool calls fail.
|
|
178
|
+
// In streaming mode, the caller decides whether to emit it.
|
|
179
|
+
for (const tc of toolCalls) {
|
|
180
|
+
this.options.onToolCall?.({
|
|
181
|
+
id: tc.id,
|
|
182
|
+
name: tc.name,
|
|
183
|
+
arguments: tc.arguments,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// Accumulate non-tool text
|
|
187
|
+
if (text) {
|
|
188
|
+
this._state.lastFullContent = this._state.lastFullContent.slice(0, this._state.lastFullContent.length - delta.content.length) + text + delta.content;
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
// Fast path: no tools, content already tracked in lastFullContent via getIncrementalDelta
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.options.onAnswer?.(delta.content);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return delta;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Feed accumulated buffer content and return any remaining text/tool calls
|
|
202
|
+
* that were not fully parsed (useful for flushing at end of stream).
|
|
203
|
+
*/
|
|
204
|
+
flush(): { text: string; toolCalls: Array<{ id: string; name: string; arguments: Record<string, unknown> }> } {
|
|
205
|
+
if (this.toolParser) {
|
|
206
|
+
return this.toolParser.flush();
|
|
207
|
+
}
|
|
208
|
+
return { text: '', toolCalls: [] };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Reset the parser state for reuse with a new stream.
|
|
213
|
+
*/
|
|
214
|
+
reset(): void {
|
|
215
|
+
this._state = {
|
|
216
|
+
targetResponseId: null,
|
|
217
|
+
currentThoughtIndex: 0,
|
|
218
|
+
lastFullContent: '',
|
|
219
|
+
reasoningBuffer: '',
|
|
220
|
+
promptTokens: this._state.promptTokens,
|
|
221
|
+
completionTokens: this._state.completionTokens,
|
|
222
|
+
};
|
|
223
|
+
this.toolParser = this.options.tools && this.options.tools.length > 0
|
|
224
|
+
? new StreamingToolParser(this.options.tools)
|
|
225
|
+
: null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// -- Private helpers --
|
|
229
|
+
|
|
230
|
+
private updateResponseId(chunk: QwenStreamChunk): void {
|
|
231
|
+
if (chunk['response.created'] && chunk['response.created'].response_id) {
|
|
232
|
+
if (!this._state.targetResponseId) {
|
|
233
|
+
this._state.targetResponseId = chunk['response.created'].response_id;
|
|
234
|
+
}
|
|
235
|
+
updateSessionParent(this.uiSessionId, chunk['response.created'].response_id);
|
|
236
|
+
this.options.onTargetResponseId?.(chunk['response.created'].response_id, this.uiSessionId);
|
|
237
|
+
} else if (chunk.response_id && !this._state.targetResponseId) {
|
|
238
|
+
this._state.targetResponseId = chunk.response_id;
|
|
239
|
+
updateSessionParent(this.uiSessionId, chunk.response_id);
|
|
240
|
+
this.options.onTargetResponseId?.(chunk.response_id, this.uiSessionId);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private updateUsage(chunk: QwenStreamChunk): void {
|
|
245
|
+
if (chunk.usage) {
|
|
246
|
+
if (chunk.usage.output_tokens) {
|
|
247
|
+
this._state.completionTokens = chunk.usage.output_tokens;
|
|
248
|
+
}
|
|
249
|
+
if (chunk.usage.input_tokens) {
|
|
250
|
+
this._state.promptTokens = chunk.usage.input_tokens;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private extractDelta(chunk: QwenStreamChunk): ParsedChunkResult | null {
|
|
256
|
+
if (!chunk.choices || !chunk.choices[0] || !chunk.choices[0].delta) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Filter by target response_id if one has been established
|
|
261
|
+
if (this._state.targetResponseId !== null && chunk.response_id !== this._state.targetResponseId) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const delta = chunk.choices[0].delta;
|
|
266
|
+
|
|
267
|
+
if (delta.phase === 'thinking_summary') {
|
|
268
|
+
if (delta.extra?.summary_thought?.content) {
|
|
269
|
+
const thoughts = delta.extra.summary_thought.content;
|
|
270
|
+
if (thoughts.length > this._state.currentThoughtIndex) {
|
|
271
|
+
const content = thoughts.slice(this._state.currentThoughtIndex).join('\n');
|
|
272
|
+
this._state.currentThoughtIndex = thoughts.length;
|
|
273
|
+
return { content, isThinking: true };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (delta.phase === 'answer') {
|
|
280
|
+
const content = delta.content ?? '';
|
|
281
|
+
return { content, isThinking: false };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* File: types.ts
|
|
3
|
+
* Project: qwenproxy
|
|
4
|
+
* Author: Pedro Farias
|
|
5
|
+
* Created: 2026-05-09
|
|
6
|
+
*
|
|
7
|
+
* Last Modified: Sat May 09 2026
|
|
8
|
+
* Modified By: Pedro Farias
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { JsonSchema, FunctionToolDefinition } from '../tools/types.ts';
|
|
12
|
+
export type { JsonSchema, FunctionToolDefinition };
|
|
13
|
+
|
|
14
|
+
/** Tool choice options */
|
|
15
|
+
export type ToolChoice = 'auto' | 'none' | 'required' | {
|
|
16
|
+
type: 'function';
|
|
17
|
+
function: { name: string };
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// --- Message Types ---
|
|
21
|
+
|
|
22
|
+
export interface ToolCallFunction {
|
|
23
|
+
name: string;
|
|
24
|
+
arguments: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface MessageToolCall {
|
|
28
|
+
id: string;
|
|
29
|
+
type: 'function';
|
|
30
|
+
function: ToolCallFunction;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Message {
|
|
34
|
+
role: string;
|
|
35
|
+
content: string | null;
|
|
36
|
+
/** Present on assistant messages that invoked tools */
|
|
37
|
+
tool_calls?: MessageToolCall[];
|
|
38
|
+
/** Present on tool/function response messages to link back to a call */
|
|
39
|
+
tool_call_id?: string;
|
|
40
|
+
/** Present on tool/function response messages */
|
|
41
|
+
name?: string;
|
|
42
|
+
/** Reasoning content for thinking models */
|
|
43
|
+
reasoning_content?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --- Request Types ---
|
|
47
|
+
|
|
48
|
+
export interface OpenAIRequest {
|
|
49
|
+
model: string;
|
|
50
|
+
messages: Message[];
|
|
51
|
+
stream?: boolean;
|
|
52
|
+
tools?: FunctionToolDefinition[];
|
|
53
|
+
tool_choice?: ToolChoice;
|
|
54
|
+
stream_options?: {
|
|
55
|
+
include_usage?: boolean;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- Response Types ---
|
|
60
|
+
|
|
61
|
+
export interface ToolCall {
|
|
62
|
+
index: number;
|
|
63
|
+
id?: string;
|
|
64
|
+
type: string;
|
|
65
|
+
function: {
|
|
66
|
+
name: string;
|
|
67
|
+
arguments: string;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ChoiceDelta {
|
|
72
|
+
role?: string;
|
|
73
|
+
content?: string | null;
|
|
74
|
+
reasoning_content?: string | null;
|
|
75
|
+
tool_calls?: ToolCall[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface Choice {
|
|
79
|
+
index: number;
|
|
80
|
+
delta?: ChoiceDelta;
|
|
81
|
+
message?: ChoiceDelta;
|
|
82
|
+
finish_reason: string | null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface Usage {
|
|
86
|
+
prompt_tokens: number;
|
|
87
|
+
completion_tokens: number;
|
|
88
|
+
total_tokens: number;
|
|
89
|
+
prompt_tokens_details?: {
|
|
90
|
+
cached_tokens: number;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface ChatCompletionChunk {
|
|
95
|
+
id: string;
|
|
96
|
+
object: string;
|
|
97
|
+
created: number;
|
|
98
|
+
model: string;
|
|
99
|
+
choices: Choice[];
|
|
100
|
+
usage?: Usage;
|
|
101
|
+
}
|