@ai-sdk/openai 3.0.13 → 3.0.15

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.
Files changed (118) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/index.d.mts +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +1 -1
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/internal/index.d.mts +1 -1
  9. package/dist/internal/index.d.ts +1 -1
  10. package/dist/internal/index.js.map +1 -1
  11. package/dist/internal/index.mjs.map +1 -1
  12. package/package.json +5 -4
  13. package/src/chat/__fixtures__/azure-model-router.1.chunks.txt +8 -0
  14. package/src/chat/__snapshots__/openai-chat-language-model.test.ts.snap +88 -0
  15. package/src/chat/convert-openai-chat-usage.ts +57 -0
  16. package/src/chat/convert-to-openai-chat-messages.test.ts +516 -0
  17. package/src/chat/convert-to-openai-chat-messages.ts +225 -0
  18. package/src/chat/get-response-metadata.ts +15 -0
  19. package/src/chat/map-openai-finish-reason.ts +19 -0
  20. package/src/chat/openai-chat-api.ts +198 -0
  21. package/src/chat/openai-chat-language-model.test.ts +3496 -0
  22. package/src/chat/openai-chat-language-model.ts +700 -0
  23. package/src/chat/openai-chat-options.ts +186 -0
  24. package/src/chat/openai-chat-prepare-tools.test.ts +322 -0
  25. package/src/chat/openai-chat-prepare-tools.ts +84 -0
  26. package/src/chat/openai-chat-prompt.ts +70 -0
  27. package/src/completion/convert-openai-completion-usage.ts +46 -0
  28. package/src/completion/convert-to-openai-completion-prompt.ts +93 -0
  29. package/src/completion/get-response-metadata.ts +15 -0
  30. package/src/completion/map-openai-finish-reason.ts +19 -0
  31. package/src/completion/openai-completion-api.ts +81 -0
  32. package/src/completion/openai-completion-language-model.test.ts +752 -0
  33. package/src/completion/openai-completion-language-model.ts +336 -0
  34. package/src/completion/openai-completion-options.ts +58 -0
  35. package/src/embedding/__snapshots__/openai-embedding-model.test.ts.snap +43 -0
  36. package/src/embedding/openai-embedding-api.ts +13 -0
  37. package/src/embedding/openai-embedding-model.test.ts +146 -0
  38. package/src/embedding/openai-embedding-model.ts +95 -0
  39. package/src/embedding/openai-embedding-options.ts +30 -0
  40. package/src/image/openai-image-api.ts +35 -0
  41. package/src/image/openai-image-model.test.ts +722 -0
  42. package/src/image/openai-image-model.ts +305 -0
  43. package/src/image/openai-image-options.ts +28 -0
  44. package/src/index.ts +9 -0
  45. package/src/internal/index.ts +19 -0
  46. package/src/openai-config.ts +18 -0
  47. package/src/openai-error.test.ts +34 -0
  48. package/src/openai-error.ts +22 -0
  49. package/src/openai-language-model-capabilities.test.ts +93 -0
  50. package/src/openai-language-model-capabilities.ts +54 -0
  51. package/src/openai-provider.test.ts +98 -0
  52. package/src/openai-provider.ts +270 -0
  53. package/src/openai-tools.ts +114 -0
  54. package/src/responses/__fixtures__/openai-apply-patch-tool-delete.1.chunks.txt +5 -0
  55. package/src/responses/__fixtures__/openai-apply-patch-tool.1.chunks.txt +38 -0
  56. package/src/responses/__fixtures__/openai-apply-patch-tool.1.json +69 -0
  57. package/src/responses/__fixtures__/openai-code-interpreter-tool.1.chunks.txt +393 -0
  58. package/src/responses/__fixtures__/openai-code-interpreter-tool.1.json +137 -0
  59. package/src/responses/__fixtures__/openai-error.1.chunks.txt +4 -0
  60. package/src/responses/__fixtures__/openai-error.1.json +8 -0
  61. package/src/responses/__fixtures__/openai-file-search-tool.1.chunks.txt +94 -0
  62. package/src/responses/__fixtures__/openai-file-search-tool.1.json +89 -0
  63. package/src/responses/__fixtures__/openai-file-search-tool.2.chunks.txt +93 -0
  64. package/src/responses/__fixtures__/openai-file-search-tool.2.json +112 -0
  65. package/src/responses/__fixtures__/openai-image-generation-tool.1.chunks.txt +16 -0
  66. package/src/responses/__fixtures__/openai-image-generation-tool.1.json +96 -0
  67. package/src/responses/__fixtures__/openai-local-shell-tool.1.chunks.txt +7 -0
  68. package/src/responses/__fixtures__/openai-local-shell-tool.1.json +70 -0
  69. package/src/responses/__fixtures__/openai-mcp-tool-approval.1.chunks.txt +11 -0
  70. package/src/responses/__fixtures__/openai-mcp-tool-approval.1.json +169 -0
  71. package/src/responses/__fixtures__/openai-mcp-tool-approval.2.chunks.txt +123 -0
  72. package/src/responses/__fixtures__/openai-mcp-tool-approval.2.json +176 -0
  73. package/src/responses/__fixtures__/openai-mcp-tool-approval.3.chunks.txt +11 -0
  74. package/src/responses/__fixtures__/openai-mcp-tool-approval.3.json +169 -0
  75. package/src/responses/__fixtures__/openai-mcp-tool-approval.4.chunks.txt +84 -0
  76. package/src/responses/__fixtures__/openai-mcp-tool-approval.4.json +182 -0
  77. package/src/responses/__fixtures__/openai-mcp-tool.1.chunks.txt +373 -0
  78. package/src/responses/__fixtures__/openai-mcp-tool.1.json +159 -0
  79. package/src/responses/__fixtures__/openai-reasoning-encrypted-content.1.chunks.txt +110 -0
  80. package/src/responses/__fixtures__/openai-reasoning-encrypted-content.1.json +117 -0
  81. package/src/responses/__fixtures__/openai-shell-tool.1.chunks.txt +182 -0
  82. package/src/responses/__fixtures__/openai-shell-tool.1.json +73 -0
  83. package/src/responses/__fixtures__/openai-web-search-tool.1.chunks.txt +185 -0
  84. package/src/responses/__fixtures__/openai-web-search-tool.1.json +266 -0
  85. package/src/responses/__snapshots__/openai-responses-language-model.test.ts.snap +10955 -0
  86. package/src/responses/convert-openai-responses-usage.ts +53 -0
  87. package/src/responses/convert-to-openai-responses-input.test.ts +2976 -0
  88. package/src/responses/convert-to-openai-responses-input.ts +578 -0
  89. package/src/responses/map-openai-responses-finish-reason.ts +22 -0
  90. package/src/responses/openai-responses-api.test.ts +89 -0
  91. package/src/responses/openai-responses-api.ts +1086 -0
  92. package/src/responses/openai-responses-language-model.test.ts +6927 -0
  93. package/src/responses/openai-responses-language-model.ts +1932 -0
  94. package/src/responses/openai-responses-options.ts +312 -0
  95. package/src/responses/openai-responses-prepare-tools.test.ts +924 -0
  96. package/src/responses/openai-responses-prepare-tools.ts +264 -0
  97. package/src/responses/openai-responses-provider-metadata.ts +39 -0
  98. package/src/speech/openai-speech-api.ts +38 -0
  99. package/src/speech/openai-speech-model.test.ts +202 -0
  100. package/src/speech/openai-speech-model.ts +137 -0
  101. package/src/speech/openai-speech-options.ts +22 -0
  102. package/src/tool/apply-patch.ts +141 -0
  103. package/src/tool/code-interpreter.ts +104 -0
  104. package/src/tool/file-search.ts +145 -0
  105. package/src/tool/image-generation.ts +126 -0
  106. package/src/tool/local-shell.test-d.ts +20 -0
  107. package/src/tool/local-shell.ts +72 -0
  108. package/src/tool/mcp.ts +125 -0
  109. package/src/tool/shell.ts +85 -0
  110. package/src/tool/web-search-preview.ts +139 -0
  111. package/src/tool/web-search.test-d.ts +13 -0
  112. package/src/tool/web-search.ts +179 -0
  113. package/src/transcription/openai-transcription-api.ts +37 -0
  114. package/src/transcription/openai-transcription-model.test.ts +507 -0
  115. package/src/transcription/openai-transcription-model.ts +232 -0
  116. package/src/transcription/openai-transcription-options.ts +50 -0
  117. package/src/transcription/transcription-test.mp3 +0 -0
  118. package/src/version.ts +6 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/openai",
3
- "version": "3.0.13",
3
+ "version": "3.0.15",
4
4
  "license": "Apache-2.0",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",
@@ -8,6 +8,7 @@
8
8
  "types": "./dist/index.d.ts",
9
9
  "files": [
10
10
  "dist/**/*",
11
+ "src",
11
12
  "CHANGELOG.md",
12
13
  "README.md",
13
14
  "internal.d.ts"
@@ -27,8 +28,8 @@
27
28
  }
28
29
  },
29
30
  "dependencies": {
30
- "@ai-sdk/provider": "3.0.4",
31
- "@ai-sdk/provider-utils": "4.0.8"
31
+ "@ai-sdk/provider-utils": "4.0.8",
32
+ "@ai-sdk/provider": "3.0.4"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@types/node": "20.17.24",
@@ -36,7 +37,7 @@
36
37
  "typescript": "5.8.3",
37
38
  "zod": "3.25.76",
38
39
  "@vercel/ai-tsconfig": "0.0.0",
39
- "@ai-sdk/test-server": "1.0.1"
40
+ "@ai-sdk/test-server": "1.0.2"
40
41
  },
41
42
  "peerDependencies": {
42
43
  "zod": "^3.25.76 || ^4.1.8"
@@ -0,0 +1,8 @@
1
+ {"choices":[],"created":0,"id":"","model":"","object":"","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"jailbreak":{"filtered":false,"detected":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}]}
2
+ {"choices":[{"content_filter_results":{},"delta":{"content":"","refusal":null,"role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":1762317021,"id":"chatcmpl-CYPS1lijGoK8gd9lYzY3r9Sx50nbt","model":"gpt-5-nano-2025-08-07","obfuscation":"D3WbtIxo1Q2j1Q","object":"chat.completion.chunk","system_fingerprint":null,"usage":null}
3
+ {"choices":[{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"delta":{"content":"Capital"},"finish_reason":null,"index":0,"logprobs":null}],"created":1762317021,"id":"chatcmpl-CYPS1lijGoK8gd9lYzY3r9Sx50nbt","model":"gpt-5-nano-2025-08-07","obfuscation":"NNpA6Dj2U","object":"chat.completion.chunk","system_fingerprint":null,"usage":null}
4
+ {"choices":[{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"delta":{"content":" of"},"finish_reason":null,"index":0,"logprobs":null}],"created":1762317021,"id":"chatcmpl-CYPS1lijGoK8gd9lYzY3r9Sx50nbt","model":"gpt-5-nano-2025-08-07","obfuscation":"etvV32yk5dbxb","object":"chat.completion.chunk","system_fingerprint":null,"usage":null}
5
+ {"choices":[{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"delta":{"content":" Denmark"},"finish_reason":null,"index":0,"logprobs":null}],"created":1762317021,"id":"chatcmpl-CYPS1lijGoK8gd9lYzY3r9Sx50nbt","model":"gpt-5-nano-2025-08-07","obfuscation":"iDOuV7Jz","object":"chat.completion.chunk","system_fingerprint":null,"usage":null}
6
+ {"choices":[{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"delta":{"content":"."},"finish_reason":null,"index":0,"logprobs":null}],"created":1762317021,"id":"chatcmpl-CYPS1lijGoK8gd9lYzY3r9Sx50nbt","model":"gpt-5-nano-2025-08-07","obfuscation":"ywLH2r1kcaeOOkq","object":"chat.completion.chunk","system_fingerprint":null,"usage":null}
7
+ {"choices":[{"content_filter_results":{},"delta":{},"finish_reason":"stop","index":0,"logprobs":null}],"created":1762317021,"id":"chatcmpl-CYPS1lijGoK8gd9lYzY3r9Sx50nbt","model":"gpt-5-nano-2025-08-07","obfuscation":"Zarov0xJhP","object":"chat.completion.chunk","system_fingerprint":null,"usage":null}
8
+ {"choices":[],"created":1762317021,"id":"chatcmpl-CYPS1lijGoK8gd9lYzY3r9Sx50nbt","model":"gpt-5-nano-2025-08-07","obfuscation":"DjqQ9RbEQMJ3PX","object":"chat.completion.chunk","system_fingerprint":null,"usage":{"completion_tokens":78,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":64,"rejected_prediction_tokens":0},"prompt_tokens":15,"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0},"total_tokens":93}}
@@ -0,0 +1,88 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`doStream > should set .modelId for model-router request 1`] = `
4
+ [
5
+ {
6
+ "type": "stream-start",
7
+ "warnings": [],
8
+ },
9
+ {
10
+ "id": "chatcmpl-CYPS1lijGoK8gd9lYzY3r9Sx50nbt",
11
+ "modelId": "gpt-5-nano-2025-08-07",
12
+ "timestamp": 2025-11-05T04:30:21.000Z,
13
+ "type": "response-metadata",
14
+ },
15
+ {
16
+ "id": "0",
17
+ "type": "text-start",
18
+ },
19
+ {
20
+ "delta": "",
21
+ "id": "0",
22
+ "type": "text-delta",
23
+ },
24
+ {
25
+ "delta": "Capital",
26
+ "id": "0",
27
+ "type": "text-delta",
28
+ },
29
+ {
30
+ "delta": " of",
31
+ "id": "0",
32
+ "type": "text-delta",
33
+ },
34
+ {
35
+ "delta": " Denmark",
36
+ "id": "0",
37
+ "type": "text-delta",
38
+ },
39
+ {
40
+ "delta": ".",
41
+ "id": "0",
42
+ "type": "text-delta",
43
+ },
44
+ {
45
+ "id": "0",
46
+ "type": "text-end",
47
+ },
48
+ {
49
+ "finishReason": {
50
+ "raw": "stop",
51
+ "unified": "stop",
52
+ },
53
+ "providerMetadata": {
54
+ "openai": {
55
+ "acceptedPredictionTokens": 0,
56
+ "rejectedPredictionTokens": 0,
57
+ },
58
+ },
59
+ "type": "finish",
60
+ "usage": {
61
+ "inputTokens": {
62
+ "cacheRead": 0,
63
+ "cacheWrite": undefined,
64
+ "noCache": 15,
65
+ "total": 15,
66
+ },
67
+ "outputTokens": {
68
+ "reasoning": 64,
69
+ "text": 14,
70
+ "total": 78,
71
+ },
72
+ "raw": {
73
+ "completion_tokens": 78,
74
+ "completion_tokens_details": {
75
+ "accepted_prediction_tokens": 0,
76
+ "reasoning_tokens": 64,
77
+ "rejected_prediction_tokens": 0,
78
+ },
79
+ "prompt_tokens": 15,
80
+ "prompt_tokens_details": {
81
+ "cached_tokens": 0,
82
+ },
83
+ "total_tokens": 93,
84
+ },
85
+ },
86
+ },
87
+ ]
88
+ `;
@@ -0,0 +1,57 @@
1
+ import { LanguageModelV3Usage } from '@ai-sdk/provider';
2
+
3
+ export type OpenAIChatUsage = {
4
+ prompt_tokens?: number | null;
5
+ completion_tokens?: number | null;
6
+ total_tokens?: number | null;
7
+ prompt_tokens_details?: {
8
+ cached_tokens?: number | null;
9
+ } | null;
10
+ completion_tokens_details?: {
11
+ reasoning_tokens?: number | null;
12
+ accepted_prediction_tokens?: number | null;
13
+ rejected_prediction_tokens?: number | null;
14
+ } | null;
15
+ };
16
+
17
+ export function convertOpenAIChatUsage(
18
+ usage: OpenAIChatUsage | undefined | null,
19
+ ): LanguageModelV3Usage {
20
+ if (usage == null) {
21
+ return {
22
+ inputTokens: {
23
+ total: undefined,
24
+ noCache: undefined,
25
+ cacheRead: undefined,
26
+ cacheWrite: undefined,
27
+ },
28
+ outputTokens: {
29
+ total: undefined,
30
+ text: undefined,
31
+ reasoning: undefined,
32
+ },
33
+ raw: undefined,
34
+ };
35
+ }
36
+
37
+ const promptTokens = usage.prompt_tokens ?? 0;
38
+ const completionTokens = usage.completion_tokens ?? 0;
39
+ const cachedTokens = usage.prompt_tokens_details?.cached_tokens ?? 0;
40
+ const reasoningTokens =
41
+ usage.completion_tokens_details?.reasoning_tokens ?? 0;
42
+
43
+ return {
44
+ inputTokens: {
45
+ total: promptTokens,
46
+ noCache: promptTokens - cachedTokens,
47
+ cacheRead: cachedTokens,
48
+ cacheWrite: undefined,
49
+ },
50
+ outputTokens: {
51
+ total: completionTokens,
52
+ text: completionTokens - reasoningTokens,
53
+ reasoning: reasoningTokens,
54
+ },
55
+ raw: usage,
56
+ };
57
+ }
@@ -0,0 +1,516 @@
1
+ import { convertToOpenAIChatMessages } from './convert-to-openai-chat-messages';
2
+ import { describe, it, expect } from 'vitest';
3
+
4
+ describe('system messages', () => {
5
+ it('should forward system messages', async () => {
6
+ const result = convertToOpenAIChatMessages({
7
+ prompt: [{ role: 'system', content: 'You are a helpful assistant.' }],
8
+ });
9
+
10
+ expect(result.messages).toEqual([
11
+ { role: 'system', content: 'You are a helpful assistant.' },
12
+ ]);
13
+ });
14
+
15
+ it('should convert system messages to developer messages when requested', async () => {
16
+ const result = convertToOpenAIChatMessages({
17
+ prompt: [{ role: 'system', content: 'You are a helpful assistant.' }],
18
+ systemMessageMode: 'developer',
19
+ });
20
+
21
+ expect(result.messages).toEqual([
22
+ { role: 'developer', content: 'You are a helpful assistant.' },
23
+ ]);
24
+ });
25
+
26
+ it('should remove system messages when requested', async () => {
27
+ const result = convertToOpenAIChatMessages({
28
+ prompt: [{ role: 'system', content: 'You are a helpful assistant.' }],
29
+ systemMessageMode: 'remove',
30
+ });
31
+
32
+ expect(result.messages).toEqual([]);
33
+ });
34
+ });
35
+
36
+ describe('user messages', () => {
37
+ it('should convert messages with only a text part to a string content', async () => {
38
+ const result = convertToOpenAIChatMessages({
39
+ prompt: [
40
+ {
41
+ role: 'user',
42
+ content: [{ type: 'text', text: 'Hello' }],
43
+ },
44
+ ],
45
+ });
46
+
47
+ expect(result.messages).toEqual([{ role: 'user', content: 'Hello' }]);
48
+ });
49
+
50
+ it('should convert messages with image parts', async () => {
51
+ const result = convertToOpenAIChatMessages({
52
+ prompt: [
53
+ {
54
+ role: 'user',
55
+ content: [
56
+ { type: 'text', text: 'Hello' },
57
+ {
58
+ type: 'file',
59
+ mediaType: 'image/png',
60
+ data: Buffer.from([0, 1, 2, 3]).toString('base64'),
61
+ },
62
+ ],
63
+ },
64
+ ],
65
+ });
66
+
67
+ expect(result.messages).toEqual([
68
+ {
69
+ role: 'user',
70
+ content: [
71
+ { type: 'text', text: 'Hello' },
72
+ {
73
+ type: 'image_url',
74
+ image_url: { url: 'data:image/png;base64,AAECAw==' },
75
+ },
76
+ ],
77
+ },
78
+ ]);
79
+ });
80
+
81
+ it('should add image detail when specified through extension', async () => {
82
+ const result = convertToOpenAIChatMessages({
83
+ prompt: [
84
+ {
85
+ role: 'user',
86
+ content: [
87
+ {
88
+ type: 'file',
89
+ mediaType: 'image/png',
90
+ data: Buffer.from([0, 1, 2, 3]).toString('base64'),
91
+ providerOptions: {
92
+ openai: {
93
+ imageDetail: 'low',
94
+ },
95
+ },
96
+ },
97
+ ],
98
+ },
99
+ ],
100
+ });
101
+
102
+ expect(result.messages).toEqual([
103
+ {
104
+ role: 'user',
105
+ content: [
106
+ {
107
+ type: 'image_url',
108
+ image_url: {
109
+ url: 'data:image/png;base64,AAECAw==',
110
+ detail: 'low',
111
+ },
112
+ },
113
+ ],
114
+ },
115
+ ]);
116
+ });
117
+
118
+ describe('file parts', () => {
119
+ it('should throw for unsupported mime types', () => {
120
+ expect(() =>
121
+ convertToOpenAIChatMessages({
122
+ prompt: [
123
+ {
124
+ role: 'user',
125
+ content: [
126
+ {
127
+ type: 'file',
128
+ data: 'AAECAw==',
129
+ mediaType: 'application/something',
130
+ },
131
+ ],
132
+ },
133
+ ],
134
+ }),
135
+ ).toThrow('file part media type application/something');
136
+ });
137
+
138
+ it('should throw for URL data', () => {
139
+ expect(() =>
140
+ convertToOpenAIChatMessages({
141
+ prompt: [
142
+ {
143
+ role: 'user',
144
+ content: [
145
+ {
146
+ type: 'file',
147
+ data: new URL('https://example.com/foo.wav'),
148
+ mediaType: 'audio/wav',
149
+ },
150
+ ],
151
+ },
152
+ ],
153
+ }),
154
+ ).toThrow('audio file parts with URLs');
155
+ });
156
+
157
+ it('should add audio content for audio/wav file parts', () => {
158
+ const result = convertToOpenAIChatMessages({
159
+ prompt: [
160
+ {
161
+ role: 'user',
162
+ content: [
163
+ {
164
+ type: 'file',
165
+ data: 'AAECAw==',
166
+ mediaType: 'audio/wav',
167
+ },
168
+ ],
169
+ },
170
+ ],
171
+ });
172
+
173
+ expect(result.messages).toEqual([
174
+ {
175
+ role: 'user',
176
+ content: [
177
+ {
178
+ type: 'input_audio',
179
+ input_audio: { data: 'AAECAw==', format: 'wav' },
180
+ },
181
+ ],
182
+ },
183
+ ]);
184
+ });
185
+
186
+ it('should add audio content for audio/mpeg file parts', () => {
187
+ const result = convertToOpenAIChatMessages({
188
+ prompt: [
189
+ {
190
+ role: 'user',
191
+ content: [
192
+ {
193
+ type: 'file',
194
+ data: 'AAECAw==',
195
+ mediaType: 'audio/mpeg',
196
+ },
197
+ ],
198
+ },
199
+ ],
200
+ });
201
+
202
+ expect(result.messages).toEqual([
203
+ {
204
+ role: 'user',
205
+ content: [
206
+ {
207
+ type: 'input_audio',
208
+ input_audio: { data: 'AAECAw==', format: 'mp3' },
209
+ },
210
+ ],
211
+ },
212
+ ]);
213
+ });
214
+
215
+ it('should add audio content for audio/mp3 file parts', () => {
216
+ const result = convertToOpenAIChatMessages({
217
+ prompt: [
218
+ {
219
+ role: 'user',
220
+ content: [
221
+ {
222
+ type: 'file',
223
+ data: 'AAECAw==',
224
+ mediaType: 'audio/mp3', // not official but sometimes used
225
+ },
226
+ ],
227
+ },
228
+ ],
229
+ });
230
+
231
+ expect(result.messages).toEqual([
232
+ {
233
+ role: 'user',
234
+ content: [
235
+ {
236
+ type: 'input_audio',
237
+ input_audio: { data: 'AAECAw==', format: 'mp3' },
238
+ },
239
+ ],
240
+ },
241
+ ]);
242
+ });
243
+
244
+ it('should convert messages with PDF file parts', async () => {
245
+ const base64Data = 'AQIDBAU='; // Base64 encoding of pdfData
246
+
247
+ const result = convertToOpenAIChatMessages({
248
+ prompt: [
249
+ {
250
+ role: 'user',
251
+ content: [
252
+ {
253
+ type: 'file',
254
+ mediaType: 'application/pdf',
255
+ data: base64Data,
256
+ filename: 'document.pdf',
257
+ },
258
+ ],
259
+ },
260
+ ],
261
+ systemMessageMode: 'system',
262
+ });
263
+
264
+ expect(result.messages).toEqual([
265
+ {
266
+ role: 'user',
267
+ content: [
268
+ {
269
+ type: 'file',
270
+ file: {
271
+ filename: 'document.pdf',
272
+ file_data: 'data:application/pdf;base64,AQIDBAU=',
273
+ },
274
+ },
275
+ ],
276
+ },
277
+ ]);
278
+ });
279
+
280
+ it('should convert messages with binary PDF file parts', async () => {
281
+ const data = Uint8Array.from([1, 2, 3, 4, 5]);
282
+
283
+ const result = convertToOpenAIChatMessages({
284
+ prompt: [
285
+ {
286
+ role: 'user',
287
+ content: [
288
+ {
289
+ type: 'file',
290
+ mediaType: 'application/pdf',
291
+ data,
292
+ filename: 'document.pdf',
293
+ },
294
+ ],
295
+ },
296
+ ],
297
+ systemMessageMode: 'system',
298
+ });
299
+
300
+ expect(result.messages).toEqual([
301
+ {
302
+ role: 'user',
303
+ content: [
304
+ {
305
+ type: 'file',
306
+ file: {
307
+ filename: 'document.pdf',
308
+ file_data: 'data:application/pdf;base64,AQIDBAU=',
309
+ },
310
+ },
311
+ ],
312
+ },
313
+ ]);
314
+ });
315
+
316
+ it('should convert messages with PDF file parts using file_id', async () => {
317
+ const result = convertToOpenAIChatMessages({
318
+ prompt: [
319
+ {
320
+ role: 'user',
321
+ content: [
322
+ {
323
+ type: 'file',
324
+ mediaType: 'application/pdf',
325
+ data: 'file-pdf-12345',
326
+ },
327
+ ],
328
+ },
329
+ ],
330
+ });
331
+
332
+ expect(result.messages).toEqual([
333
+ {
334
+ role: 'user',
335
+ content: [
336
+ {
337
+ type: 'file',
338
+ file: {
339
+ file_id: 'file-pdf-12345',
340
+ },
341
+ },
342
+ ],
343
+ },
344
+ ]);
345
+ });
346
+
347
+ it('should use default filename for PDF file parts when not provided', async () => {
348
+ const base64Data = 'AQIDBAU=';
349
+
350
+ const result = convertToOpenAIChatMessages({
351
+ prompt: [
352
+ {
353
+ role: 'user',
354
+ content: [
355
+ {
356
+ type: 'file',
357
+ mediaType: 'application/pdf',
358
+ data: base64Data,
359
+ },
360
+ ],
361
+ },
362
+ ],
363
+ systemMessageMode: 'system',
364
+ });
365
+
366
+ expect(result.messages).toEqual([
367
+ {
368
+ role: 'user',
369
+ content: [
370
+ {
371
+ type: 'file',
372
+ file: {
373
+ filename: 'part-0.pdf',
374
+ file_data: 'data:application/pdf;base64,AQIDBAU=',
375
+ },
376
+ },
377
+ ],
378
+ },
379
+ ]);
380
+ });
381
+
382
+ it('should throw error for unsupported file types', async () => {
383
+ const base64Data = 'AQIDBAU=';
384
+
385
+ expect(() => {
386
+ convertToOpenAIChatMessages({
387
+ prompt: [
388
+ {
389
+ role: 'user',
390
+ content: [
391
+ {
392
+ type: 'file',
393
+ mediaType: 'text/plain',
394
+ data: base64Data,
395
+ },
396
+ ],
397
+ },
398
+ ],
399
+ systemMessageMode: 'system',
400
+ });
401
+ }).toThrow('file part media type text/plain');
402
+ });
403
+
404
+ it('should throw error for file URLs', async () => {
405
+ expect(() => {
406
+ convertToOpenAIChatMessages({
407
+ prompt: [
408
+ {
409
+ role: 'user',
410
+ content: [
411
+ {
412
+ type: 'file',
413
+ mediaType: 'application/pdf',
414
+ data: new URL('https://example.com/document.pdf'),
415
+ },
416
+ ],
417
+ },
418
+ ],
419
+ systemMessageMode: 'system',
420
+ });
421
+ }).toThrow('PDF file parts with URLs');
422
+ });
423
+ });
424
+ });
425
+
426
+ describe('tool calls', () => {
427
+ it('should stringify arguments to tool calls', () => {
428
+ const result = convertToOpenAIChatMessages({
429
+ prompt: [
430
+ {
431
+ role: 'assistant',
432
+ content: [
433
+ {
434
+ type: 'tool-call',
435
+ input: { foo: 'bar123' },
436
+ toolCallId: 'quux',
437
+ toolName: 'thwomp',
438
+ },
439
+ ],
440
+ },
441
+ {
442
+ role: 'tool',
443
+ content: [
444
+ {
445
+ type: 'tool-result',
446
+ toolCallId: 'quux',
447
+ toolName: 'thwomp',
448
+ output: { type: 'json', value: { oof: '321rab' } },
449
+ },
450
+ ],
451
+ },
452
+ ],
453
+ });
454
+
455
+ expect(result.messages).toEqual([
456
+ {
457
+ role: 'assistant',
458
+ content: '',
459
+ tool_calls: [
460
+ {
461
+ type: 'function',
462
+ id: 'quux',
463
+ function: {
464
+ name: 'thwomp',
465
+ arguments: JSON.stringify({ foo: 'bar123' }),
466
+ },
467
+ },
468
+ ],
469
+ },
470
+ {
471
+ role: 'tool',
472
+ content: JSON.stringify({ oof: '321rab' }),
473
+ tool_call_id: 'quux',
474
+ },
475
+ ]);
476
+ });
477
+
478
+ it('should handle different tool output types', () => {
479
+ const result = convertToOpenAIChatMessages({
480
+ prompt: [
481
+ {
482
+ role: 'tool',
483
+ content: [
484
+ {
485
+ type: 'tool-result',
486
+ toolCallId: 'text-tool',
487
+ toolName: 'text-tool',
488
+ output: { type: 'text', value: 'Hello world' },
489
+ },
490
+ {
491
+ type: 'tool-result',
492
+ toolCallId: 'error-tool',
493
+ toolName: 'error-tool',
494
+ output: { type: 'error-text', value: 'Something went wrong' },
495
+ },
496
+ ],
497
+ },
498
+ ],
499
+ });
500
+
501
+ expect(result.messages).toMatchInlineSnapshot(`
502
+ [
503
+ {
504
+ "content": "Hello world",
505
+ "role": "tool",
506
+ "tool_call_id": "text-tool",
507
+ },
508
+ {
509
+ "content": "Something went wrong",
510
+ "role": "tool",
511
+ "tool_call_id": "error-tool",
512
+ },
513
+ ]
514
+ `);
515
+ });
516
+ });