@oevortex/opencode-qwen-auth 0.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 +21 -0
- package/README.md +220 -0
- package/index.ts +82 -0
- package/package.json +40 -0
- package/src/constants.ts +43 -0
- package/src/global.d.ts +257 -0
- package/src/models.ts +148 -0
- package/src/plugin/auth.ts +151 -0
- package/src/plugin/browser.ts +126 -0
- package/src/plugin/fetch-wrapper.ts +460 -0
- package/src/plugin/logger.ts +111 -0
- package/src/plugin/server.ts +364 -0
- package/src/plugin/token.ts +225 -0
- package/src/plugin.ts +444 -0
- package/src/qwen/oauth.ts +271 -0
- package/src/qwen/thinking-parser.ts +190 -0
- package/src/types.ts +292 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thinking Block Parser for Qwen
|
|
3
|
+
* Handles <think>...</think> tags in the stream to separate
|
|
4
|
+
* reasoning content from regular text content.
|
|
5
|
+
*
|
|
6
|
+
* Based on revibe/core/llm/backend/qwen/handler.py
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface ParseResult {
|
|
10
|
+
/** Regular content (outside thinking blocks) */
|
|
11
|
+
regularContent: string;
|
|
12
|
+
/** Reasoning/thinking content (inside thinking blocks) */
|
|
13
|
+
reasoningContent: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parser for Qwen's thinking/reasoning blocks.
|
|
18
|
+
*
|
|
19
|
+
* Handles `<think>...</think>` tags in the stream to separate
|
|
20
|
+
* reasoning content from regular text content.
|
|
21
|
+
*
|
|
22
|
+
* This parser is stateful and designed for streaming scenarios
|
|
23
|
+
* where tags might be split across multiple chunks.
|
|
24
|
+
*/
|
|
25
|
+
export class ThinkingBlockParser {
|
|
26
|
+
private inThinkingBlock = false;
|
|
27
|
+
private buffer = "";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Reset the parser state.
|
|
31
|
+
* Call this when starting a new message/response.
|
|
32
|
+
*/
|
|
33
|
+
reset(): void {
|
|
34
|
+
this.inThinkingBlock = false;
|
|
35
|
+
this.buffer = "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if currently inside a thinking block.
|
|
40
|
+
*/
|
|
41
|
+
isInThinkingBlock(): boolean {
|
|
42
|
+
return this.inThinkingBlock;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse text and separate thinking content from regular content.
|
|
47
|
+
*
|
|
48
|
+
* This method is designed for streaming - call it with each chunk
|
|
49
|
+
* of text as it arrives, and it will return the parsed content.
|
|
50
|
+
*
|
|
51
|
+
* @param text - The text chunk to parse
|
|
52
|
+
* @returns Object containing regular and reasoning content
|
|
53
|
+
*/
|
|
54
|
+
parse(text: string): ParseResult {
|
|
55
|
+
let regularContent = "";
|
|
56
|
+
let reasoningContent = "";
|
|
57
|
+
|
|
58
|
+
// Add to buffer and process
|
|
59
|
+
this.buffer += text;
|
|
60
|
+
|
|
61
|
+
while (true) {
|
|
62
|
+
if (this.inThinkingBlock) {
|
|
63
|
+
// Look for closing tag
|
|
64
|
+
const endIdx = this.buffer.indexOf("</think>");
|
|
65
|
+
if (endIdx !== -1) {
|
|
66
|
+
reasoningContent += this.buffer.slice(0, endIdx);
|
|
67
|
+
this.buffer = this.buffer.slice(endIdx + 8); // len("</think>") = 8
|
|
68
|
+
this.inThinkingBlock = false;
|
|
69
|
+
} else {
|
|
70
|
+
// Still in thinking block, emit all as reasoning
|
|
71
|
+
reasoningContent += this.buffer;
|
|
72
|
+
this.buffer = "";
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
// Look for opening tag
|
|
77
|
+
const startIdx = this.buffer.indexOf("<think>");
|
|
78
|
+
if (startIdx !== -1) {
|
|
79
|
+
regularContent += this.buffer.slice(0, startIdx);
|
|
80
|
+
this.buffer = this.buffer.slice(startIdx + 7); // len("<think>") = 7
|
|
81
|
+
this.inThinkingBlock = true;
|
|
82
|
+
} else {
|
|
83
|
+
// Check for partial opening tag at the end of buffer
|
|
84
|
+
// This handles cases where "<think>" is split across chunks
|
|
85
|
+
const partialTag = this.findPartialOpenTag();
|
|
86
|
+
if (partialTag > 0) {
|
|
87
|
+
// Keep potential partial tag in buffer, emit the rest
|
|
88
|
+
regularContent += this.buffer.slice(0, -partialTag);
|
|
89
|
+
this.buffer = this.buffer.slice(-partialTag);
|
|
90
|
+
} else {
|
|
91
|
+
// No thinking block, emit all as regular content
|
|
92
|
+
regularContent += this.buffer;
|
|
93
|
+
this.buffer = "";
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { regularContent, reasoningContent };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Find if there's a partial opening tag at the end of the buffer.
|
|
105
|
+
* Returns the length of the partial tag, or 0 if none found.
|
|
106
|
+
*/
|
|
107
|
+
private findPartialOpenTag(): number {
|
|
108
|
+
const tag = "<think>";
|
|
109
|
+
for (let len = Math.min(tag.length - 1, this.buffer.length); len > 0; len--) {
|
|
110
|
+
const suffix = this.buffer.slice(-len);
|
|
111
|
+
if (tag.startsWith(suffix)) {
|
|
112
|
+
return len;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Flush any remaining content in the buffer.
|
|
120
|
+
* Call this when the stream is complete.
|
|
121
|
+
*
|
|
122
|
+
* @returns Any remaining content based on current state
|
|
123
|
+
*/
|
|
124
|
+
flush(): ParseResult {
|
|
125
|
+
const result: ParseResult = {
|
|
126
|
+
regularContent: "",
|
|
127
|
+
reasoningContent: "",
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
if (this.buffer.length > 0) {
|
|
131
|
+
if (this.inThinkingBlock) {
|
|
132
|
+
result.reasoningContent = this.buffer;
|
|
133
|
+
} else {
|
|
134
|
+
result.regularContent = this.buffer;
|
|
135
|
+
}
|
|
136
|
+
this.buffer = "";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Parse complete text for thinking blocks (non-streaming).
|
|
145
|
+
*
|
|
146
|
+
* Use this for non-streaming responses where the entire text is available.
|
|
147
|
+
*
|
|
148
|
+
* @param text - The complete text to parse
|
|
149
|
+
* @returns Object containing regular and reasoning content
|
|
150
|
+
*/
|
|
151
|
+
export function parseThinkingBlocks(text: string): ParseResult {
|
|
152
|
+
const parser = new ThinkingBlockParser();
|
|
153
|
+
const result = parser.parse(text);
|
|
154
|
+
const flushed = parser.flush();
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
regularContent: result.regularContent + flushed.regularContent,
|
|
158
|
+
reasoningContent: result.reasoningContent + flushed.reasoningContent,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Remove thinking blocks from text, returning only regular content.
|
|
164
|
+
*
|
|
165
|
+
* @param text - The text to process
|
|
166
|
+
* @returns Text with thinking blocks removed
|
|
167
|
+
*/
|
|
168
|
+
export function stripThinkingBlocks(text: string): string {
|
|
169
|
+
return parseThinkingBlocks(text).regularContent;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Extract only thinking/reasoning content from text.
|
|
174
|
+
*
|
|
175
|
+
* @param text - The text to process
|
|
176
|
+
* @returns Only the content inside thinking blocks
|
|
177
|
+
*/
|
|
178
|
+
export function extractThinkingContent(text: string): string {
|
|
179
|
+
return parseThinkingBlocks(text).reasoningContent;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Check if text contains any thinking blocks.
|
|
184
|
+
*
|
|
185
|
+
* @param text - The text to check
|
|
186
|
+
* @returns True if text contains <think> tags
|
|
187
|
+
*/
|
|
188
|
+
export function hasThinkingBlocks(text: string): boolean {
|
|
189
|
+
return text.includes("<think>") || text.includes("</think>");
|
|
190
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qwen OAuth and plugin types
|
|
3
|
+
* Based on revibe/core/llm/backend/qwen implementation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ============================================================
|
|
7
|
+
// OAuth Types
|
|
8
|
+
// ============================================================
|
|
9
|
+
|
|
10
|
+
export interface QwenOAuthCredentials {
|
|
11
|
+
access_token: string;
|
|
12
|
+
refresh_token: string;
|
|
13
|
+
token_type: string;
|
|
14
|
+
expiry_date: number; // Timestamp in milliseconds
|
|
15
|
+
resource_url?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface QwenTokenResponse {
|
|
19
|
+
access_token: string;
|
|
20
|
+
refresh_token?: string;
|
|
21
|
+
token_type: string;
|
|
22
|
+
expires_in: number; // Seconds until expiration
|
|
23
|
+
error?: string;
|
|
24
|
+
error_description?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ============================================================
|
|
28
|
+
// Model Types
|
|
29
|
+
// ============================================================
|
|
30
|
+
|
|
31
|
+
export interface QwenModelInfo {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
context_window?: number;
|
|
35
|
+
max_output?: number;
|
|
36
|
+
input_price?: number; // Per million tokens
|
|
37
|
+
output_price?: number; // Per million tokens
|
|
38
|
+
supports_native_tools?: boolean;
|
|
39
|
+
supports_thinking?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ============================================================
|
|
43
|
+
// Stream Chunk Types
|
|
44
|
+
// ============================================================
|
|
45
|
+
|
|
46
|
+
export type StreamChunkType =
|
|
47
|
+
| "text"
|
|
48
|
+
| "reasoning"
|
|
49
|
+
| "thinking_complete"
|
|
50
|
+
| "usage"
|
|
51
|
+
| "tool_call"
|
|
52
|
+
| "tool_call_start"
|
|
53
|
+
| "tool_call_delta"
|
|
54
|
+
| "tool_call_end"
|
|
55
|
+
| "tool_call_partial"
|
|
56
|
+
| "error";
|
|
57
|
+
|
|
58
|
+
export interface StreamTextChunk {
|
|
59
|
+
type: "text";
|
|
60
|
+
text: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface StreamReasoningChunk {
|
|
64
|
+
type: "reasoning";
|
|
65
|
+
text: string;
|
|
66
|
+
signature?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface StreamUsageChunk {
|
|
70
|
+
type: "usage";
|
|
71
|
+
input_tokens: number;
|
|
72
|
+
output_tokens: number;
|
|
73
|
+
cache_write_tokens?: number;
|
|
74
|
+
cache_read_tokens?: number;
|
|
75
|
+
reasoning_tokens?: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface StreamToolCallPartialChunk {
|
|
79
|
+
type: "tool_call_partial";
|
|
80
|
+
index: number;
|
|
81
|
+
id?: string;
|
|
82
|
+
name?: string;
|
|
83
|
+
arguments?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface StreamErrorChunk {
|
|
87
|
+
type: "error";
|
|
88
|
+
error: string;
|
|
89
|
+
message: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type StreamChunk =
|
|
93
|
+
| StreamTextChunk
|
|
94
|
+
| StreamReasoningChunk
|
|
95
|
+
| StreamUsageChunk
|
|
96
|
+
| StreamToolCallPartialChunk
|
|
97
|
+
| StreamErrorChunk;
|
|
98
|
+
|
|
99
|
+
// ============================================================
|
|
100
|
+
// API Request/Response Types
|
|
101
|
+
// ============================================================
|
|
102
|
+
|
|
103
|
+
export interface ChatMessage {
|
|
104
|
+
role: "system" | "user" | "assistant" | "tool";
|
|
105
|
+
content?: string;
|
|
106
|
+
reasoning_content?: string;
|
|
107
|
+
tool_calls?: ToolCall[];
|
|
108
|
+
tool_call_id?: string;
|
|
109
|
+
name?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface ToolCall {
|
|
113
|
+
id?: string;
|
|
114
|
+
index?: number;
|
|
115
|
+
type?: "function";
|
|
116
|
+
function: FunctionCall;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface FunctionCall {
|
|
120
|
+
name?: string;
|
|
121
|
+
arguments?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface Tool {
|
|
125
|
+
type: "function";
|
|
126
|
+
function: {
|
|
127
|
+
name: string;
|
|
128
|
+
description?: string;
|
|
129
|
+
parameters?: Record<string, unknown>;
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type ToolChoice = "none" | "auto" | "required" | { type: "function"; function: { name: string } };
|
|
134
|
+
|
|
135
|
+
export interface ChatCompletionRequest {
|
|
136
|
+
model: string;
|
|
137
|
+
messages: ChatMessage[];
|
|
138
|
+
temperature?: number;
|
|
139
|
+
max_tokens?: number;
|
|
140
|
+
tools?: Tool[];
|
|
141
|
+
tool_choice?: ToolChoice;
|
|
142
|
+
stream?: boolean;
|
|
143
|
+
stream_options?: {
|
|
144
|
+
include_usage?: boolean;
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface ChatCompletionChoice {
|
|
149
|
+
index: number;
|
|
150
|
+
message?: ChatMessage;
|
|
151
|
+
delta?: Partial<ChatMessage>;
|
|
152
|
+
finish_reason?: string | null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface ChatCompletionUsage {
|
|
156
|
+
prompt_tokens: number;
|
|
157
|
+
completion_tokens: number;
|
|
158
|
+
total_tokens?: number;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface ChatCompletionResponse {
|
|
162
|
+
id: string;
|
|
163
|
+
object: string;
|
|
164
|
+
created: number;
|
|
165
|
+
model: string;
|
|
166
|
+
choices: ChatCompletionChoice[];
|
|
167
|
+
usage?: ChatCompletionUsage;
|
|
168
|
+
error?: {
|
|
169
|
+
message: string;
|
|
170
|
+
type?: string;
|
|
171
|
+
code?: string;
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface ChatCompletionChunk {
|
|
176
|
+
id: string;
|
|
177
|
+
object: string;
|
|
178
|
+
created: number;
|
|
179
|
+
model: string;
|
|
180
|
+
choices: ChatCompletionChoice[];
|
|
181
|
+
usage?: ChatCompletionUsage;
|
|
182
|
+
error?: {
|
|
183
|
+
message: string;
|
|
184
|
+
type?: string;
|
|
185
|
+
code?: string;
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ============================================================
|
|
190
|
+
// Plugin Types
|
|
191
|
+
// ============================================================
|
|
192
|
+
|
|
193
|
+
export interface AuthDetails {
|
|
194
|
+
type: "oauth" | "api";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface OAuthAuthDetails extends AuthDetails {
|
|
198
|
+
type: "oauth";
|
|
199
|
+
access?: string;
|
|
200
|
+
refresh: string;
|
|
201
|
+
expires?: number;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface ApiAuthDetails extends AuthDetails {
|
|
205
|
+
type: "api";
|
|
206
|
+
apiKey: string;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface RefreshParts {
|
|
210
|
+
refreshToken: string;
|
|
211
|
+
resourceUrl?: string;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface LoaderResult {
|
|
215
|
+
apiKey?: string;
|
|
216
|
+
baseUrl?: string;
|
|
217
|
+
fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface Provider {
|
|
221
|
+
models?: Record<string, { cost?: { input: number; output: number } } | undefined>;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export type GetAuth = () => Promise<AuthDetails>;
|
|
225
|
+
|
|
226
|
+
export interface PluginContext {
|
|
227
|
+
client: {
|
|
228
|
+
auth: {
|
|
229
|
+
set: (options: { path: { id: string }; body: Record<string, unknown> }) => Promise<void>;
|
|
230
|
+
};
|
|
231
|
+
tui: {
|
|
232
|
+
showToast: (options: { body: { message: string; variant: "info" | "success" | "warning" | "error" } }) => Promise<void>;
|
|
233
|
+
};
|
|
234
|
+
log?: {
|
|
235
|
+
debug: (message: string, meta?: Record<string, unknown>) => void;
|
|
236
|
+
info: (message: string, meta?: Record<string, unknown>) => void;
|
|
237
|
+
warn: (message: string, meta?: Record<string, unknown>) => void;
|
|
238
|
+
error: (message: string, meta?: Record<string, unknown>) => void;
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export interface AuthorizeResult {
|
|
244
|
+
url: string;
|
|
245
|
+
instructions?: string;
|
|
246
|
+
method: "auto" | "manual";
|
|
247
|
+
callback: () => Promise<TokenExchangeResult>;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export interface TokenExchangeResult {
|
|
251
|
+
type: "success" | "failed";
|
|
252
|
+
refresh?: string;
|
|
253
|
+
access?: string;
|
|
254
|
+
expires?: number;
|
|
255
|
+
error?: string;
|
|
256
|
+
resourceUrl?: string;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export interface AuthMethod {
|
|
260
|
+
label: string;
|
|
261
|
+
type: "oauth" | "api";
|
|
262
|
+
authorize?: () => Promise<AuthorizeResult>;
|
|
263
|
+
prompts?: Array<{
|
|
264
|
+
type: "text" | "password";
|
|
265
|
+
message: string;
|
|
266
|
+
key: string;
|
|
267
|
+
}>;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export interface PluginResult {
|
|
271
|
+
auth?: {
|
|
272
|
+
provider: string;
|
|
273
|
+
loader: (getAuth: GetAuth, provider: Provider) => Promise<LoaderResult | Record<string, unknown>>;
|
|
274
|
+
methods: AuthMethod[];
|
|
275
|
+
};
|
|
276
|
+
tool?: Record<string, unknown>;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ============================================================
|
|
280
|
+
// Rate Limit Types
|
|
281
|
+
// ============================================================
|
|
282
|
+
|
|
283
|
+
export interface RateLimitState {
|
|
284
|
+
consecutive429: number;
|
|
285
|
+
lastAt: number;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export interface RateLimitDelay {
|
|
289
|
+
attempt: number;
|
|
290
|
+
serverRetryAfterMs: number | null;
|
|
291
|
+
delayMs: number;
|
|
292
|
+
}
|