diff --git a/packages/ai/test/executor.test.ts b/packages/ai/test/executor.test.ts index 709afbcafc9..014cba9bc9e 100644 --- a/packages/ai/test/executor.test.ts +++ b/packages/ai/test/executor.test.ts @@ -574,6 +574,7 @@ describe("WebSocket channel execution", () => { const model = configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).responses("gpt-4.1-mini") const request = LLM.request({ model, prompt: "Say hello." }) const frames = [ + JSON.stringify({ type: "response.output_item.added", item: { type: "message", id: "msg_1" } }), JSON.stringify({ type: "response.output_text.delta", item_id: "msg_1", delta: "Hi" }), JSON.stringify({ type: "response.completed", response: { id: "resp_1" } }), ] diff --git a/packages/ai/test/fixtures/recordings/anthropic-messages-cache/keeps-a-long-tool-turn-inside-the-cache-lookback.json b/packages/ai/test/fixtures/recordings/anthropic-messages-cache/keeps-a-long-tool-turn-inside-the-cache-lookback.json index f42f76ed3b9..06a607e5235 100644 --- a/packages/ai/test/fixtures/recordings/anthropic-messages-cache/keeps-a-long-tool-turn-inside-the-cache-lookback.json +++ b/packages/ai/test/fixtures/recordings/anthropic-messages-cache/keeps-a-long-tool-turn-inside-the-cache-lookback.json @@ -10,7 +10,7 @@ "transport": "http", "request": { "method": "POST", - "url": "https://api.anthropic.com/v1/messages", + "url": "https://api.anthropic.com/v1/messages?beta=true", "headers": { "anthropic-version": "2023-06-01", "content-type": "application/json" @@ -29,7 +29,7 @@ "transport": "http", "request": { "method": "POST", - "url": "https://api.anthropic.com/v1/messages", + "url": "https://api.anthropic.com/v1/messages?beta=true", "headers": { "anthropic-version": "2023-06-01", "content-type": "application/json" diff --git a/packages/ai/test/fixtures/recordings/anthropic-messages-cache/writes-then-reads-cache-control-on-identical-second-call.json b/packages/ai/test/fixtures/recordings/anthropic-messages-cache/writes-then-reads-cache-control-on-identical-second-call.json index 8cf2be05c14..ab90170a626 100644 --- a/packages/ai/test/fixtures/recordings/anthropic-messages-cache/writes-then-reads-cache-control-on-identical-second-call.json +++ b/packages/ai/test/fixtures/recordings/anthropic-messages-cache/writes-then-reads-cache-control-on-identical-second-call.json @@ -10,7 +10,7 @@ "transport": "http", "request": { "method": "POST", - "url": "https://api.anthropic.com/v1/messages", + "url": "https://api.anthropic.com/v1/messages?beta=true", "headers": { "anthropic-version": "2023-06-01", "content-type": "application/json" @@ -29,7 +29,7 @@ "transport": "http", "request": { "method": "POST", - "url": "https://api.anthropic.com/v1/messages", + "url": "https://api.anthropic.com/v1/messages?beta=true", "headers": { "anthropic-version": "2023-06-01", "content-type": "application/json" diff --git a/packages/ai/test/fixtures/recordings/anthropic-messages/anthropic-opus-4-7-image-tool-result.json b/packages/ai/test/fixtures/recordings/anthropic-messages/anthropic-opus-4-7-image-tool-result.json index b1ba048d7a3..1b58484b397 100644 --- a/packages/ai/test/fixtures/recordings/anthropic-messages/anthropic-opus-4-7-image-tool-result.json +++ b/packages/ai/test/fixtures/recordings/anthropic-messages/anthropic-opus-4-7-image-tool-result.json @@ -24,7 +24,7 @@ "transport": "http", "request": { "method": "POST", - "url": "https://api.anthropic.com/v1/messages", + "url": "https://api.anthropic.com/v1/messages?beta=true", "headers": { "anthropic-version": "2023-06-01", "content-type": "application/json" diff --git a/packages/ai/test/fixtures/recordings/openrouter-reasoning-tool-loop.json b/packages/ai/test/fixtures/recordings/openrouter-reasoning-tool-loop.json index 343f4d81608..1d6c337d952 100644 --- a/packages/ai/test/fixtures/recordings/openrouter-reasoning-tool-loop.json +++ b/packages/ai/test/fixtures/recordings/openrouter-reasoning-tool-loop.json @@ -23,7 +23,7 @@ "headers": { "content-type": "application/json" }, - "body": "{\"model\":\"anthropic/claude-sonnet-4.6\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the get_weather tool exactly once. After the tool result, reply exactly: Paris is sunny.\"},{\"role\":\"user\",\"content\":\"What is the weather in Paris?\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":1536,\"reasoning\":{\"max_tokens\":1024}}" + "body": "{\"model\":\"anthropic/claude-sonnet-4.6\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the get_weather tool exactly once. After the tool result, reply exactly: Paris is sunny.\"},{\"role\":\"user\",\"content\":\"What is the weather in Paris?\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"reasoning\":{\"max_tokens\":1024},\"max_completion_tokens\":1536,\"store\":false,\"usage\":{\"include\":true}}" }, "response": { "status": 200, @@ -41,7 +41,7 @@ "headers": { "content-type": "application/json" }, - "body": "{\"model\":\"anthropic/claude-sonnet-4.6\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the get_weather tool exactly once. After the tool result, reply exactly: Paris is sunny.\"},{\"role\":\"user\",\"content\":\"What is the weather in Paris?\"},{\"role\":\"assistant\",\"content\":\"Sure! Let me check the weather in Paris for you right now!\",\"tool_calls\":[{\"id\":\"toolu_01PaChhcyw3yu2P2bDS2bgAA\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"}}],\"reasoning\":\"The user wants to know the weather in Paris. I'll use the get_weather tool.\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\"The user wants to know the weather in Paris. I'll use the get_weather tool.\",\"format\":\"anthropic-claude-v1\",\"index\":0,\"signature\":\"ErkCCosBCA8YAipAjKnRKpxkZ4eHrMPJ63IWEOYPSzb+XSHyG+vLK+2ks2O9T4N9M37Xn2kausQSH1rfsrdmKxgUlBg6yUFRgMVR7DIRY2xhdWRlLXNvbm5ldC00LTY4AEIIdGhpbmtpbmdaJDRjMGYwNDZmLTI1ZmQtNDVmYi1iZmIzLWEwOGE4ZTI0OWNhNxIMNFb5O6pb4nX0HojdGgyL5h+CAIpsxpdM1QgiMGm/i3ST6F5mAhxB+Uez0Cm95ra9yvQkrzHaA/AmWoXpdmPlczSn1S1RDk2IqeA57Spbf7JT44jygtLQt6yZmGzoTBHn3VkwaNZsuuAtbdo4B5QJXooa/AoKKs54QZ2kfS640vsv5flQVCg7CoQCFuLKjIeLMO7MnxVyuskXJr1DgesTa7I0ScF53U9JGhgB\"}]},{\"role\":\"tool\",\"tool_call_id\":\"toolu_01PaChhcyw3yu2P2bDS2bgAA\",\"content\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":1536,\"reasoning\":{\"max_tokens\":1024}}" + "body": "{\"model\":\"anthropic/claude-sonnet-4.6\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the get_weather tool exactly once. After the tool result, reply exactly: Paris is sunny.\"},{\"role\":\"user\",\"content\":\"What is the weather in Paris?\"},{\"role\":\"assistant\",\"content\":\"Sure! Let me check the weather in Paris for you right now!\",\"tool_calls\":[{\"id\":\"toolu_01PaChhcyw3yu2P2bDS2bgAA\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"}}],\"reasoning\":\"The user wants to know the weather in Paris. I'll use the get_weather tool.\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\"The user wants to know the weather in Paris. I'll use the get_weather tool.\",\"format\":\"anthropic-claude-v1\",\"index\":0,\"signature\":\"ErkCCosBCA8YAipAjKnRKpxkZ4eHrMPJ63IWEOYPSzb+XSHyG+vLK+2ks2O9T4N9M37Xn2kausQSH1rfsrdmKxgUlBg6yUFRgMVR7DIRY2xhdWRlLXNvbm5ldC00LTY4AEIIdGhpbmtpbmdaJDRjMGYwNDZmLTI1ZmQtNDVmYi1iZmIzLWEwOGE4ZTI0OWNhNxIMNFb5O6pb4nX0HojdGgyL5h+CAIpsxpdM1QgiMGm/i3ST6F5mAhxB+Uez0Cm95ra9yvQkrzHaA/AmWoXpdmPlczSn1S1RDk2IqeA57Spbf7JT44jygtLQt6yZmGzoTBHn3VkwaNZsuuAtbdo4B5QJXooa/AoKKs54QZ2kfS640vsv5flQVCg7CoQCFuLKjIeLMO7MnxVyuskXJr1DgesTa7I0ScF53U9JGhgB\"}]},{\"role\":\"tool\",\"tool_call_id\":\"toolu_01PaChhcyw3yu2P2bDS2bgAA\",\"content\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"reasoning\":{\"max_tokens\":1024},\"max_completion_tokens\":1536,\"store\":false,\"usage\":{\"include\":true}}" }, "response": { "status": 200, diff --git a/packages/ai/test/fixtures/recordings/openrouter-reasoning.json b/packages/ai/test/fixtures/recordings/openrouter-reasoning.json index 3be1e9f1be3..852a6f6b9f3 100644 --- a/packages/ai/test/fixtures/recordings/openrouter-reasoning.json +++ b/packages/ai/test/fixtures/recordings/openrouter-reasoning.json @@ -15,7 +15,7 @@ "headers": { "content-type": "application/json" }, - "body": "{\"model\":\"anthropic/claude-sonnet-4.6\",\"messages\":[{\"role\":\"system\",\"content\":\"Think through the arithmetic, then reply with only the final integer.\"},{\"role\":\"user\",\"content\":\"What is 173 multiplied by 219?\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":1536,\"temperature\":0,\"reasoning\":{\"max_tokens\":1024}}" + "body": "{\"model\":\"anthropic/claude-sonnet-4.6\",\"messages\":[{\"role\":\"system\",\"content\":\"Think through the arithmetic, then reply with only the final integer.\"},{\"role\":\"user\",\"content\":\"What is 173 multiplied by 219?\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"temperature\":0,\"reasoning\":{\"max_tokens\":1024},\"max_completion_tokens\":1536,\"store\":false,\"usage\":{\"include\":true}}" }, "response": { "status": 200, diff --git a/packages/ai/test/fixtures/recordings/pdf/anthropic-tool-result.json b/packages/ai/test/fixtures/recordings/pdf/anthropic-tool-result.json index a3f0ec9fc40..d2b09152b88 100644 --- a/packages/ai/test/fixtures/recordings/pdf/anthropic-tool-result.json +++ b/packages/ai/test/fixtures/recordings/pdf/anthropic-tool-result.json @@ -10,11 +10,11 @@ "transport": "http", "request": { "method": "POST", - "url": "https://api.anthropic.com/v1/messages", + "url": "https://api.anthropic.com/v1/messages?beta=true", "headers": { "content-type": "application/json" }, - "body": "{\"model\":\"claude-haiku-4-5-20251001\",\"system\":[{\"type\":\"text\",\"text\":\"Read the PDF returned by the tool and follow the user's response format exactly.\"}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Return only the verification code from the PDF.\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"call_pdf_1\",\"name\":\"read_pdf\",\"input\":{}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"call_pdf_1\",\"content\":[{\"type\":\"text\",\"text\":\"PDF read successfully\"},{\"type\":\"document\",\"source\":{\"type\":\"base64\",\"media_type\":\"application/pdf\",\"data\":\"JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvUmVzb3VyY2VzIDw8IC9Gb250IDw8IC9GMSA1IDAgUiA+PiA+PiAvQ29udGVudHMgNCAwIFIgPj4KZW5kb2JqCjQgMCBvYmoKPDwgL0xlbmd0aCA3NSA+PgpzdHJlYW0KQlQKL0YxIDE4IFRmCjcyIDcyMCBUZAooUERGIGNhc3NldHRlIHZlcmlmaWNhdGlvbiBjb2RlOiBPUkNISUQtNzM5MSkgVGoKRVQKZW5kc3RyZWFtCmVuZG9iago1IDAgb2JqCjw8IC9UeXBlIC9Gb250IC9TdWJ0eXBlIC9UeXBlMSAvQmFzZUZvbnQgL0hlbHZldGljYSA+PgplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAowMDAwMDAwMjQxIDAwMDAwIG4gCjAwMDAwMDAzNjUgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA2IC9Sb290IDEgMCBSID4+CnN0YXJ0eHJlZgo0MzUKJSVFT0YK\"}}]}]}],\"tools\":[{\"name\":\"read_pdf\",\"description\":\"Read the attached PDF.\",\"input_schema\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}}],\"stream\":true,\"max_tokens\":40,\"temperature\":0}" + "body": "{\"model\":\"claude-haiku-4-5-20251001\",\"system\":[{\"type\":\"text\",\"text\":\"Read the PDF returned by the tool and follow the user's response format exactly.\"}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Return only the verification code from the PDF.\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"call_pdf_1\",\"name\":\"read_pdf\",\"input\":{}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"call_pdf_1\",\"content\":[{\"type\":\"text\",\"text\":\"PDF read successfully\"},{\"type\":\"document\",\"source\":{\"type\":\"base64\",\"media_type\":\"application/pdf\",\"data\":\"JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvUmVzb3VyY2VzIDw8IC9Gb250IDw8IC9GMSA1IDAgUiA+PiA+PiAvQ29udGVudHMgNCAwIFIgPj4KZW5kb2JqCjQgMCBvYmoKPDwgL0xlbmd0aCA3NSA+PgpzdHJlYW0KQlQKL0YxIDE4IFRmCjcyIDcyMCBUZAooUERGIGNhc3NldHRlIHZlcmlmaWNhdGlvbiBjb2RlOiBPUkNISUQtNzM5MSkgVGoKRVQKZW5kc3RyZWFtCmVuZG9iago1IDAgb2JqCjw8IC9UeXBlIC9Gb250IC9TdWJ0eXBlIC9UeXBlMSAvQmFzZUZvbnQgL0hlbHZldGljYSA+PgplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAowMDAwMDAwMjQxIDAwMDAwIG4gCjAwMDAwMDAzNjUgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA2IC9Sb290IDEgMCBSID4+CnN0YXJ0eHJlZgo0MzUKJSVFT0YK\"},\"title\":\"verification.pdf\"}]}]}],\"tools\":[{\"name\":\"read_pdf\",\"description\":\"Read the attached PDF.\",\"input_schema\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}}],\"stream\":true,\"max_tokens\":40,\"temperature\":0}" }, "response": { "status": 200, diff --git a/packages/ai/test/fixtures/recordings/pdf/anthropic-user-input.json b/packages/ai/test/fixtures/recordings/pdf/anthropic-user-input.json index e7951bfa9f9..cc89432b61c 100644 --- a/packages/ai/test/fixtures/recordings/pdf/anthropic-user-input.json +++ b/packages/ai/test/fixtures/recordings/pdf/anthropic-user-input.json @@ -10,11 +10,11 @@ "transport": "http", "request": { "method": "POST", - "url": "https://api.anthropic.com/v1/messages", + "url": "https://api.anthropic.com/v1/messages?beta=true", "headers": { "content-type": "application/json" }, - "body": "{\"model\":\"claude-haiku-4-5-20251001\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"document\",\"source\":{\"type\":\"base64\",\"media_type\":\"application/pdf\",\"data\":\"JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvUmVzb3VyY2VzIDw8IC9Gb250IDw8IC9GMSA1IDAgUiA+PiA+PiAvQ29udGVudHMgNCAwIFIgPj4KZW5kb2JqCjQgMCBvYmoKPDwgL0xlbmd0aCA3NSA+PgpzdHJlYW0KQlQKL0YxIDE4IFRmCjcyIDcyMCBUZAooUERGIGNhc3NldHRlIHZlcmlmaWNhdGlvbiBjb2RlOiBPUkNISUQtNzM5MSkgVGoKRVQKZW5kc3RyZWFtCmVuZG9iago1IDAgb2JqCjw8IC9UeXBlIC9Gb250IC9TdWJ0eXBlIC9UeXBlMSAvQmFzZUZvbnQgL0hlbHZldGljYSA+PgplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAowMDAwMDAwMjQxIDAwMDAwIG4gCjAwMDAwMDAzNjUgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA2IC9Sb290IDEgMCBSID4+CnN0YXJ0eHJlZgo0MzUKJSVFT0YK\"}},{\"type\":\"text\",\"text\":\"Return only the verification code from the PDF.\"}]}],\"stream\":true,\"max_tokens\":40,\"temperature\":0}" + "body": "{\"model\":\"claude-haiku-4-5-20251001\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"document\",\"source\":{\"type\":\"base64\",\"media_type\":\"application/pdf\",\"data\":\"JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvUmVzb3VyY2VzIDw8IC9Gb250IDw8IC9GMSA1IDAgUiA+PiA+PiAvQ29udGVudHMgNCAwIFIgPj4KZW5kb2JqCjQgMCBvYmoKPDwgL0xlbmd0aCA3NSA+PgpzdHJlYW0KQlQKL0YxIDE4IFRmCjcyIDcyMCBUZAooUERGIGNhc3NldHRlIHZlcmlmaWNhdGlvbiBjb2RlOiBPUkNISUQtNzM5MSkgVGoKRVQKZW5kc3RyZWFtCmVuZG9iago1IDAgb2JqCjw8IC9UeXBlIC9Gb250IC9TdWJ0eXBlIC9UeXBlMSAvQmFzZUZvbnQgL0hlbHZldGljYSA+PgplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAowMDAwMDAwMjQxIDAwMDAwIG4gCjAwMDAwMDAzNjUgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA2IC9Sb290IDEgMCBSID4+CnN0YXJ0eHJlZgo0MzUKJSVFT0YK\"},\"title\":\"verification.pdf\"},{\"type\":\"text\",\"text\":\"Return only the verification code from the PDF.\"}]}],\"stream\":true,\"max_tokens\":40,\"temperature\":0}" }, "response": { "status": 200, diff --git a/packages/ai/test/provider-package.test.ts b/packages/ai/test/provider-package.test.ts index b7d63585b12..993319f7b29 100644 --- a/packages/ai/test/provider-package.test.ts +++ b/packages/ai/test/provider-package.test.ts @@ -1,5 +1,7 @@ import { describe, expect, test } from "bun:test" import { model } from "@opencode-ai/ai/providers/openai" +import { LLM } from "../src/index.js" +import { Endpoint } from "../src/route/endpoint.js" describe("provider package entrypoints", () => { test("semantic API aliases expose the same contract", async () => { @@ -36,7 +38,8 @@ describe("provider package entrypoints", () => { expect(modules[0].model).toBe(modules[1].model) expect(modules[8].model).toBe(modules[9].model) expect(modules[12].model).toBe(modules[13].model) - expect(modules[19].model).toBe(modules[20].model) + expect(modules[19].model).toBe(modules[21].model) + expect(modules[19].model).not.toBe(modules[20].model) }) test("maps DeepInfra package settings onto its native executable model", async () => { @@ -139,8 +142,10 @@ describe("provider package entrypoints", () => { expect(selected.route.id).toBe("anthropic-messages") expect(selected.route.endpoint).toMatchObject({ baseURL: "https://messages.example.test/v1", - path: "/messages", }) + expect( + Endpoint.render(selected.route.endpoint, { request: LLM.request({ model: selected }), body: {} }).toString(), + ).toBe("https://messages.example.test/v1/messages") expect(selected.route.defaults.headers).toEqual({ "x-application": "opencode" }) expect(selected.route.defaults.http?.body).toEqual({ metadata: { user_id: "user_1" } }) expect(selected.route.defaults.providerOptions).toEqual({ effort: "low" }) diff --git a/packages/ai/test/provider/openrouter.test.ts b/packages/ai/test/provider/openrouter.test.ts index 357363e3602..b2ae31413ce 100644 --- a/packages/ai/test/provider/openrouter.test.ts +++ b/packages/ai/test/provider/openrouter.test.ts @@ -305,9 +305,11 @@ describe("OpenRouter", () => { expect(prepared.body.messages).toEqual([ { role: "assistant", - content: null, + content: "", reasoning: "Thinking", + reasoning_content: undefined, reasoning_details: details, + reasoning_text: undefined, }, ]) }), @@ -335,7 +337,14 @@ describe("OpenRouter", () => { ) expect(prepared.body.messages).toEqual([ - { role: "assistant", content: null, reasoning: "Thinking", reasoning_details: details }, + { + role: "assistant", + content: "", + reasoning: "Thinking", + reasoning_content: undefined, + reasoning_details: details, + reasoning_text: undefined, + }, ]) }), ) @@ -361,7 +370,14 @@ describe("OpenRouter", () => { ) expect(prepared.body.messages).toEqual([ - { role: "assistant", content: null, reasoning: "AB", reasoning_details: details }, + { + role: "assistant", + content: "", + reasoning: "AB", + reasoning_content: undefined, + reasoning_details: details, + reasoning_text: undefined, + }, ]) }), ) @@ -376,7 +392,16 @@ describe("OpenRouter", () => { }), ) - expect(prepared.body.messages).toEqual([{ role: "assistant", content: null }]) + expect(prepared.body.messages).toEqual([ + { + role: "assistant", + content: "", + reasoning: undefined, + reasoning_content: undefined, + reasoning_details: undefined, + reasoning_text: undefined, + }, + ]) }), ) }) diff --git a/packages/httpapi-codegen/test/generate.test.ts b/packages/httpapi-codegen/test/generate.test.ts index 7b8f5a7af24..ba61a507043 100644 --- a/packages/httpapi-codegen/test/generate.test.ts +++ b/packages/httpapi-codegen/test/generate.test.ts @@ -669,8 +669,8 @@ describe("HttpApiCodegen.generate", () => { ) const types = output.files.find((file) => file.path === "types.ts")?.content - expect(types).toContain('readonly "first": ({ readonly "value": string })') - expect(types).toContain('readonly "second": ({ readonly "value": string })') + expect(types).toContain('readonly "first": { readonly "value": string }') + expect(types).toContain('readonly "second": { readonly "value": string }') expect(types).not.toContain("export type Objects") }) diff --git a/packages/server/test/fetch.test.ts b/packages/server/test/fetch.test.ts index 840787592dc..6d4805bf948 100644 --- a/packages/server/test/fetch.test.ts +++ b/packages/server/test/fetch.test.ts @@ -1,5 +1,5 @@ import { expect } from "bun:test" -import { createServer, type Server } from "node:http" +import { createServer } from "node:http" import { makeMemoryDriver } from "@opencode-ai/core/environment/index" import { Workspace } from "@opencode-ai/core/workspace" import { WorkspaceDriver } from "@opencode-ai/core/workspace/driver" @@ -10,15 +10,44 @@ import { ServerFetch } from "../src/fetch" const options = { app: { version: "test-version" }, database: { path: ":memory:" }, + models: { fetch: false }, fs: { filewatcher: false }, } as const type Handler = (request: Request) => Promise -function occupy(server: Server, port: number) { - return Effect.callback((resume) => { - server.once("error", (error) => resume(Effect.fail(error))) - server.listen(port, "localhost", () => resume(Effect.void)) +function occupy(port: number, cancel = false) { + return Effect.gen(function* () { + const requests: string[] = [] + // A localhost listener occupies only one family; Bun can bind the other. + const servers = ["127.0.0.1", "::1"].map((host) => ({ + host, + server: createServer((request, response) => { + requests.push(request.url ?? "") + response.end(cancel ? "cancelled" : "still running", () => { + if (cancel) servers.forEach((item) => item.server.close()) + }) + }), + })) + yield* Effect.addFinalizer(() => + Effect.forEach(servers, (item) => + Effect.callback((resume) => { + item.server.close(() => resume(Effect.void)) + item.server.closeAllConnections() + }), + ), + ) + yield* Effect.forEach(servers, (item) => + Effect.callback((resume) => { + const onError = (error: Error) => resume(Effect.fail(error)) + item.server.once("error", onError) + item.server.listen(port, item.host, () => { + item.server.off("error", onError) + resume(Effect.void) + }) + }), + ) + return requests }) } @@ -98,13 +127,7 @@ it.live("serves unauthenticated and answers CORS preflight when no password is c it.live("cancels a stale OpenAI OAuth callback server before falling back", () => Effect.gen(function* () { - const requests: string[] = [] - const blocker = createServer((request, response) => { - requests.push(request.url ?? "") - response.end("cancelled", () => blocker.close()) - }) - yield* occupy(blocker, 1455) - yield* Effect.addFinalizer(() => Effect.sync(() => blocker.close())) + const requests = yield* occupy(1455, true) const handler = yield* ServerFetch.make(options) yield* ready(handler) const response = yield* connectOpenAI(handler) @@ -118,13 +141,7 @@ it.live("cancels a stale OpenAI OAuth callback server before falling back", () = it.live("falls back to port 1457 when OpenAI OAuth port 1455 remains busy", () => Effect.gen(function* () { - const requests: string[] = [] - const blocker = createServer((request, response) => { - requests.push(request.url ?? "") - response.end("still running") - }) - yield* occupy(blocker, 1455) - yield* Effect.addFinalizer(() => Effect.sync(() => blocker.close())) + const requests = yield* occupy(1455) const handler = yield* ServerFetch.make(options) yield* ready(handler) const response = yield* connectOpenAI(handler) @@ -138,12 +155,8 @@ it.live("falls back to port 1457 when OpenAI OAuth port 1455 remains busy", () = it.live("explains how to recover when both OpenAI OAuth callback ports are busy", () => Effect.gen(function* () { - const preferred = createServer((_request, response) => response.end("still running")) - const fallback = createServer() - yield* occupy(preferred, 1455) - yield* occupy(fallback, 1457) - yield* Effect.addFinalizer(() => Effect.sync(() => preferred.close())) - yield* Effect.addFinalizer(() => Effect.sync(() => fallback.close())) + yield* occupy(1455) + yield* occupy(1457) const handler = yield* ServerFetch.make(options) yield* ready(handler) const response = yield* connectOpenAI(handler)