Skip to content

Commit c4c9007

Browse files
authored
refactor: reorganize directory structure and fix Qwen 3.5 compatibility (#14)
* feat(provider): introduce Qwen error handling and refactor provider structure - Added `error.ts` for Qwen error response schema and handling. - Created `provider.ts` to encapsulate QwenProvider functionality, including model creation methods. - Updated `index.ts` to export new provider structure and removed deprecated references. - Removed `map-qwen-finish-reason.ts` as it is no longer needed. - Added comprehensive tests for the new provider structure and error handling. * chore: lint fix
1 parent 1cfa1d9 commit c4c9007

26 files changed

Lines changed: 618 additions & 288 deletions

src/__tests__/integration/reranking.integration.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77
import { rerank } from "ai"
88
import { describe, expect, it } from "vitest"
9-
import { createQwen } from "../../qwen-provider"
9+
import { createQwen } from "../../provider"
1010
import "dotenv/config"
1111

1212
// Skip if no API key

src/__tests__/qwen-chat-language-model.test.ts renamed to src/__tests__/models/chat.test.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
import type { LanguageModelV3Prompt } from "@ai-sdk/provider"
33
import { convertReadableStreamToArray } from "@ai-sdk/provider-utils/test"
44
import { beforeEach, describe, expect, it, vi } from "vitest"
5-
import { QwenChatLanguageModel } from "../qwen-chat-language-model"
6-
import { createQwen } from "../qwen-provider"
5+
import { QwenChatLanguageModel } from "../../models/chat"
6+
import { createQwen } from "../../provider"
77

88
const TEST_PROMPT: LanguageModelV3Prompt = [
99
{ role: "user", content: [{ type: "text", text: "Hello" }] },
@@ -301,6 +301,7 @@ describe("doGenerate", () => {
301301

302302
expect(requestBody).toStrictEqual({
303303
model: "qwen-chat",
304+
extra_body: { enable_thinking: false },
304305
messages: [{ role: "user", content: "Hello" }],
305306
})
306307
})
@@ -316,6 +317,7 @@ describe("doGenerate", () => {
316317

317318
expect(requestBody).toStrictEqual({
318319
model: "qwen-chat",
320+
extra_body: { enable_thinking: false },
319321
messages: [{ role: "user", content: "Hello" }],
320322
user: "test-user-id",
321323
})
@@ -335,6 +337,7 @@ describe("doGenerate", () => {
335337

336338
expect(requestBody).toStrictEqual({
337339
model: "qwen-chat",
340+
extra_body: { enable_thinking: false },
338341
messages: [{ role: "user", content: "Hello" }],
339342
someCustomOption: "test-value",
340343
})
@@ -354,6 +357,7 @@ describe("doGenerate", () => {
354357

355358
expect(requestBody).toStrictEqual({
356359
model: "qwen-chat",
360+
extra_body: { enable_thinking: false },
357361
messages: [{ role: "user", content: "Hello" }],
358362
})
359363
})
@@ -385,6 +389,7 @@ describe("doGenerate", () => {
385389

386390
expect(requestBody).toStrictEqual({
387391
model: "qwen-chat",
392+
extra_body: { enable_thinking: false },
388393
messages: [{ role: "user", content: "Hello" }],
389394
tools: [
390395
{
@@ -557,6 +562,7 @@ describe("doGenerate", () => {
557562

558563
expect(requestBody).toStrictEqual({
559564
model: "qwen-plus",
565+
extra_body: { enable_thinking: false },
560566
messages: [{ role: "user", content: "Hello" }],
561567
})
562568
})
@@ -573,6 +579,7 @@ describe("doGenerate", () => {
573579

574580
expect(requestBody).toStrictEqual({
575581
model: "qwen-plus",
582+
extra_body: { enable_thinking: false },
576583
messages: [{ role: "user", content: "Hello" }],
577584
response_format: { type: "json_object" },
578585
})
@@ -616,6 +623,7 @@ describe("doGenerate", () => {
616623

617624
expect(requestBody).toStrictEqual({
618625
model: "qwen-plus",
626+
extra_body: { enable_thinking: false },
619627
messages: [{ role: "user", content: "Hello" }],
620628
response_format: { type: "json_object" },
621629
})
@@ -668,6 +676,7 @@ describe("doGenerate", () => {
668676

669677
expect(requestBody).toStrictEqual({
670678
model: "qwen-plus",
679+
extra_body: { enable_thinking: false },
671680
messages: [{ role: "user", content: "Hello" }],
672681
response_format: {
673682
type: "json_schema",
@@ -727,6 +736,7 @@ describe("doGenerate", () => {
727736

728737
expect(requestBody).toStrictEqual({
729738
model: "qwen-plus",
739+
extra_body: { enable_thinking: false },
730740
messages: [{ role: "user", content: "Hello" }],
731741
response_format: {
732742
type: "json_schema",
@@ -776,6 +786,7 @@ describe("doGenerate", () => {
776786

777787
expect(requestBody).toStrictEqual({
778788
model: "qwen-plus",
789+
extra_body: { enable_thinking: false },
779790
messages: [{ role: "user", content: "Hello" }],
780791
response_format: {
781792
type: "json_object",
@@ -793,7 +804,7 @@ describe("doGenerate", () => {
793804
})
794805

795806
expect(request).toMatchObject({
796-
body: "{\"model\":\"qwen-chat\",\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}]}",
807+
body: "{\"model\":\"qwen-chat\",\"extra_body\":{\"enable_thinking\":false},\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}]}",
797808
})
798809
})
799810
})
@@ -1125,7 +1136,10 @@ describe("doStream", () => {
11251136

11261137
const parts = await convertReadableStreamToArray(stream)
11271138

1128-
expect(parts.find(p => p.type === "error")).toBeFalsy()
1139+
expect(parts).toContainEqual({
1140+
type: "error",
1141+
error: "InternalServerError: list index out of range",
1142+
})
11291143
expect(parts).toContainEqual({
11301144
type: "tool-input-end",
11311145
id: "call_recover_1",
@@ -1138,15 +1152,15 @@ describe("doStream", () => {
11381152
})
11391153
expect(parts).toContainEqual({
11401154
type: "finish",
1141-
finishReason: { unified: "tool-calls", raw: "tool_calls" },
1155+
finishReason: { unified: "other", raw: undefined },
11421156
usage: {
11431157
inputTokens: { total: undefined, noCache: undefined, cacheRead: undefined, cacheWrite: undefined },
11441158
outputTokens: { total: undefined, text: undefined, reasoning: undefined },
11451159
},
11461160
})
11471161
})
11481162

1149-
it("should retry doStream request on `500 + list index out of range`", async () => {
1163+
it("should throw on first `500 + list index out of range` for doStream request", async () => {
11501164
let attempts = 0
11511165

11521166
const provider = createQwen({
@@ -1187,14 +1201,10 @@ describe("doStream", () => {
11871201
})
11881202

11891203
const model = provider("qwen-chat")
1190-
const { stream } = await model.doStream({
1204+
await expect(model.doStream({
11911205
prompt: TEST_PROMPT,
1192-
})
1193-
1194-
const parts = await convertReadableStreamToArray(stream)
1195-
1196-
expect(attempts).toBe(2)
1197-
expect(parts.find(p => p.type === "finish")).toBeTruthy()
1206+
})).rejects.toThrow("InternalServerError: list index out of range")
1207+
expect(attempts).toBe(1)
11981208
})
11991209

12001210
it("should stream tool deltas with V3 format", async () => {
@@ -1422,7 +1432,7 @@ describe("doStream", () => {
14221432
type: "tool-call",
14231433
toolCallId: "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa",
14241434
toolName: "searchGoogle",
1425-
input: "{\"query\": \"latest news on ai\"}",
1435+
input: "{\"query\":\"latest news on ai\"}",
14261436
})
14271437
})
14281438

@@ -1534,6 +1544,7 @@ describe("doStream", () => {
15341544
include_usage: true,
15351545
},
15361546
model: "qwen-chat",
1547+
extra_body: { enable_thinking: false },
15371548
messages: [{ role: "user", content: "Hello" }],
15381549
})
15391550
})
@@ -1579,6 +1590,7 @@ describe("doStream", () => {
15791590
include_usage: true,
15801591
},
15811592
model: "qwen-chat",
1593+
extra_body: { enable_thinking: false },
15821594
messages: [{ role: "user", content: "Hello" }],
15831595
someCustomOption: "test-value",
15841596
})
@@ -1602,6 +1614,7 @@ describe("doStream", () => {
16021614
include_usage: true,
16031615
},
16041616
model: "qwen-chat",
1617+
extra_body: { enable_thinking: false },
16051618
messages: [{ role: "user", content: "Hello" }],
16061619
})
16071620
})
@@ -1615,7 +1628,7 @@ describe("doStream", () => {
16151628
})
16161629

16171630
expect(request).toMatchObject({
1618-
body: "{\"model\":\"qwen-chat\",\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true}}",
1631+
body: "{\"model\":\"qwen-chat\",\"extra_body\":{\"enable_thinking\":false},\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true}}",
16191632
})
16201633
})
16211634
})
@@ -1923,6 +1936,7 @@ describe("metadata extraction", () => {
19231936

19241937
expect(requestBody).toStrictEqual({
19251938
model: "qwen-plus",
1939+
extra_body: { enable_thinking: false },
19261940
messages: [{ role: "user", content: "Hello" }],
19271941
})
19281942
})
@@ -1993,6 +2007,7 @@ describe("metadata extraction", () => {
19932007

19942008
expect(requestBody).toStrictEqual({
19952009
model: "qwen-plus",
2010+
extra_body: { enable_thinking: false },
19962011
messages: [{ role: "user", content: "Hello" }],
19972012
stream: true,
19982013
stream_options: {

src/__tests__/qwen-completion-language-model.test.ts renamed to src/__tests__/models/completion.test.ts

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
import type { LanguageModelV3Prompt } from "@ai-sdk/provider"
33
import { convertReadableStreamToArray } from "@ai-sdk/provider-utils/test"
44
import { beforeEach, describe, expect, it, vi } from "vitest"
5-
import { QwenCompletionLanguageModel } from "../qwen-completion-language-model"
6-
import { createQwen } from "../qwen-provider"
5+
import { QwenCompletionLanguageModel } from "../../models/completion"
6+
import { createQwen } from "../../provider"
77

88
const TEST_PROMPT: LanguageModelV3Prompt = [
99
{ role: "user", content: [{ type: "text", text: "Hello" }] },
@@ -394,6 +394,58 @@ describe("doStream", () => {
394394
})
395395
})
396396

397+
it("should throw on retryable errors from stream request", async () => {
398+
let fetchCalls = 0
399+
400+
const provider = createQwen({
401+
baseURL: "https://my.api.com/v1/",
402+
headers: {
403+
Authorization: `Bearer test-api-key`,
404+
},
405+
fetch: async (_url, init) => {
406+
fetchCalls++
407+
408+
if (init?.body) {
409+
requestBody = JSON.parse(init.body as string)
410+
}
411+
412+
if (fetchCalls === 1) {
413+
return new Response(JSON.stringify({
414+
object: "error",
415+
message: "InternalServerError: list index out of range",
416+
type: "InternalServerError",
417+
param: null,
418+
code: null,
419+
}), {
420+
status: 500,
421+
headers: { "content-type": "application/json" },
422+
})
423+
}
424+
425+
const encoder = new TextEncoder()
426+
const stream = new ReadableStream({
427+
start(controller) {
428+
for (const chunk of responseChunks) {
429+
controller.enqueue(encoder.encode(chunk))
430+
}
431+
controller.close()
432+
},
433+
})
434+
435+
return new Response(stream, {
436+
headers: responseHeaders,
437+
})
438+
},
439+
})
440+
441+
const model = provider.completion("qwen-plus")
442+
await expect(model.doStream({
443+
prompt: TEST_PROMPT,
444+
})).rejects.toThrow("InternalServerError: list index out of range")
445+
expect(fetchCalls).toBe(1)
446+
expect(requestBody).toMatchObject({ stream: true })
447+
})
448+
397449
it("should handle unparsable stream parts", async () => {
398450
responseChunks = [`data: {unparsable}\n\n`, "data: [DONE]\n\n"]
399451
const provider = createStreamingTestProvider()

src/__tests__/qwen-embedding-model.test.ts renamed to src/__tests__/models/embedding.test.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { TooManyEmbeddingValuesForCallError } from "@ai-sdk/provider"
22
import { beforeEach, describe, expect, it, vi } from "vitest"
3-
import { createQwen } from "../qwen-provider"
3+
import { createQwen } from "../../provider"
44

55
const dummyEmbeddings = [
66
[0.1, 0.2, 0.3, 0.4, 0.5],
@@ -169,4 +169,45 @@ describe("doEmbed", () => {
169169
async () => await model.doEmbed({ values: many }),
170170
).rejects.toBeInstanceOf(TooManyEmbeddingValuesForCallError)
171171
})
172+
173+
it("should throw on retryable errors from embed request", async () => {
174+
let fetchCalls = 0
175+
176+
const provider = createQwen({
177+
baseURL: "https://my.api.com/v1/",
178+
headers: {
179+
Authorization: `Bearer test-api-key`,
180+
},
181+
fetch: async (_url, init) => {
182+
fetchCalls++
183+
if (init?.body) {
184+
requestBody = JSON.parse(init.body as string)
185+
}
186+
187+
if (fetchCalls === 1) {
188+
return new Response(JSON.stringify({
189+
object: "error",
190+
message: "Service Unavailable",
191+
type: "ServiceUnavailable",
192+
param: null,
193+
code: null,
194+
}), {
195+
status: 503,
196+
headers: { "content-type": "application/json" },
197+
})
198+
}
199+
200+
return new Response(JSON.stringify(responseBody), {
201+
headers: responseHeaders,
202+
})
203+
},
204+
})
205+
206+
const model = provider.textEmbeddingModel("text-embedding-3-large")
207+
await expect(model.doEmbed({ values: testValues })).rejects.toThrow(
208+
"Service Unavailable",
209+
)
210+
expect(fetchCalls).toBe(1)
211+
expect(requestBody).toMatchObject({ model: "text-embedding-3-large" })
212+
})
172213
})

0 commit comments

Comments
 (0)