@robota-sdk/agent-provider 3.0.0-beta.64

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 (220) hide show
  1. package/LICENSE +21 -0
  2. package/dist/browser/index.d.ts +1104 -0
  3. package/dist/browser/index.d.ts.map +1 -0
  4. package/dist/browser/index.js +7 -0
  5. package/dist/browser/index.js.map +1 -0
  6. package/dist/loggers/index.cjs +1 -0
  7. package/dist/loggers/index.d.ts +151 -0
  8. package/dist/loggers/index.d.ts.map +1 -0
  9. package/dist/loggers/index.js +2 -0
  10. package/dist/loggers/index.js.map +1 -0
  11. package/dist/node/anthropic/index.cjs +1 -0
  12. package/dist/node/anthropic/index.d.ts +158 -0
  13. package/dist/node/anthropic/index.d.ts.map +1 -0
  14. package/dist/node/anthropic/index.js +1 -0
  15. package/dist/node/anthropic--1vgLC-e.js +5 -0
  16. package/dist/node/anthropic--1vgLC-e.js.map +1 -0
  17. package/dist/node/anthropic-BFQ6DSCP.cjs +4 -0
  18. package/dist/node/bytedance/index.cjs +1 -0
  19. package/dist/node/bytedance/index.d.ts +74 -0
  20. package/dist/node/bytedance/index.d.ts.map +1 -0
  21. package/dist/node/bytedance/index.js +1 -0
  22. package/dist/node/bytedance-C_0sF_pJ.js +2 -0
  23. package/dist/node/bytedance-C_0sF_pJ.js.map +1 -0
  24. package/dist/node/bytedance-DVPxqEiC.cjs +1 -0
  25. package/dist/node/chunk-Bmb41Sf3.cjs +1 -0
  26. package/dist/node/deepseek/index.cjs +1 -0
  27. package/dist/node/deepseek/index.d.ts +2 -0
  28. package/dist/node/deepseek/index.js +1 -0
  29. package/dist/node/deepseek-_8Ixx7rA.js +2 -0
  30. package/dist/node/deepseek-_8Ixx7rA.js.map +1 -0
  31. package/dist/node/deepseek-oA2Y6bD0.cjs +1 -0
  32. package/dist/node/gemini/index.cjs +1 -0
  33. package/dist/node/gemini/index.d.ts +173 -0
  34. package/dist/node/gemini/index.d.ts.map +1 -0
  35. package/dist/node/gemini/index.js +1 -0
  36. package/dist/node/gemini-Bh2U87MY.js +4 -0
  37. package/dist/node/gemini-Bh2U87MY.js.map +1 -0
  38. package/dist/node/gemini-DSaNCxZj.cjs +3 -0
  39. package/dist/node/gemma/index.cjs +1 -0
  40. package/dist/node/gemma/index.d.ts +2 -0
  41. package/dist/node/gemma/index.js +1 -0
  42. package/dist/node/gemma-Dp_AfCUR.js +2 -0
  43. package/dist/node/gemma-Dp_AfCUR.js.map +1 -0
  44. package/dist/node/gemma-G-Pf_PnX.cjs +1 -0
  45. package/dist/node/google/index.cjs +1 -0
  46. package/dist/node/google/index.d.ts +14 -0
  47. package/dist/node/google/index.d.ts.map +1 -0
  48. package/dist/node/google/index.js +2 -0
  49. package/dist/node/google/index.js.map +1 -0
  50. package/dist/node/index-B6PnlDMd.d.ts +82 -0
  51. package/dist/node/index-B6PnlDMd.d.ts.map +1 -0
  52. package/dist/node/index-B7UvPJcI.d.ts +315 -0
  53. package/dist/node/index-B7UvPJcI.d.ts.map +1 -0
  54. package/dist/node/index-BLPOTNb5.d.ts +98 -0
  55. package/dist/node/index-BLPOTNb5.d.ts.map +1 -0
  56. package/dist/node/index-BqixM_XD.d.ts +231 -0
  57. package/dist/node/index-BqixM_XD.d.ts.map +1 -0
  58. package/dist/node/index-C3beaqKO.d.ts +231 -0
  59. package/dist/node/index-C3beaqKO.d.ts.map +1 -0
  60. package/dist/node/index-Cp2XRh9G.d.ts +82 -0
  61. package/dist/node/index-Cp2XRh9G.d.ts.map +1 -0
  62. package/dist/node/index-DSv5xruI.d.ts +98 -0
  63. package/dist/node/index-DSv5xruI.d.ts.map +1 -0
  64. package/dist/node/index-w0bV1uaP.d.ts +315 -0
  65. package/dist/node/index-w0bV1uaP.d.ts.map +1 -0
  66. package/dist/node/index.cjs +1 -0
  67. package/dist/node/index.d.ts +8 -0
  68. package/dist/node/index.js +1 -0
  69. package/dist/node/openai/index.cjs +1 -0
  70. package/dist/node/openai/index.d.ts +2 -0
  71. package/dist/node/openai/index.js +1 -0
  72. package/dist/node/openai-CRQjg4xF.js +2 -0
  73. package/dist/node/openai-CRQjg4xF.js.map +1 -0
  74. package/dist/node/openai-compatible-BYfyY5lb.cjs +1 -0
  75. package/dist/node/openai-compatible-Dm4Sof9e.js +2 -0
  76. package/dist/node/openai-compatible-Dm4Sof9e.js.map +1 -0
  77. package/dist/node/openai-xWC6pY7r.cjs +1 -0
  78. package/dist/node/qwen/index.cjs +1 -0
  79. package/dist/node/qwen/index.d.ts +2 -0
  80. package/dist/node/qwen/index.js +1 -0
  81. package/dist/node/qwen-ChUZobTL.js +2 -0
  82. package/dist/node/qwen-ChUZobTL.js.map +1 -0
  83. package/dist/node/qwen-CjT71vSM.cjs +1 -0
  84. package/package.json +157 -0
  85. package/src/anthropic/__tests__/abort-streaming.test.ts +199 -0
  86. package/src/anthropic/__tests__/model-catalog-refresh.test.ts +92 -0
  87. package/src/anthropic/__tests__/provider-definition.test.ts +55 -0
  88. package/src/anthropic/__tests__/provider.test.ts +1357 -0
  89. package/src/anthropic/__tests__/response-parser.test.ts +326 -0
  90. package/src/anthropic/index.ts +22 -0
  91. package/src/anthropic/message-converter.ts +181 -0
  92. package/src/anthropic/model-catalog-refresh.ts +128 -0
  93. package/src/anthropic/parsers/response-parser.ts +184 -0
  94. package/src/anthropic/provider-definition.ts +93 -0
  95. package/src/anthropic/provider.ts +290 -0
  96. package/src/anthropic/streaming-handler.ts +204 -0
  97. package/src/anthropic/types/api-types.ts +158 -0
  98. package/src/anthropic/types.ts +79 -0
  99. package/src/bytedance/http-client.test.ts +288 -0
  100. package/src/bytedance/http-client.ts +163 -0
  101. package/src/bytedance/index.ts +2 -0
  102. package/src/bytedance/provider.spec.ts +320 -0
  103. package/src/bytedance/provider.ts +171 -0
  104. package/src/bytedance/status-mapper.test.ts +299 -0
  105. package/src/bytedance/status-mapper.ts +141 -0
  106. package/src/bytedance/types.ts +68 -0
  107. package/src/deepseek/defaults.ts +4 -0
  108. package/src/deepseek/index.ts +22 -0
  109. package/src/deepseek/model-catalog-refresh.test.ts +57 -0
  110. package/src/deepseek/model-catalog-refresh.ts +105 -0
  111. package/src/deepseek/model-catalog.ts +55 -0
  112. package/src/deepseek/provider-definition.test.ts +109 -0
  113. package/src/deepseek/provider-definition.ts +132 -0
  114. package/src/deepseek/provider.test.ts +324 -0
  115. package/src/deepseek/provider.ts +298 -0
  116. package/src/deepseek/types.ts +37 -0
  117. package/src/gemini/execution-helpers.ts +233 -0
  118. package/src/gemini/genai-transport.test.ts +208 -0
  119. package/src/gemini/image-operations.test.ts +448 -0
  120. package/src/gemini/image-operations.ts +261 -0
  121. package/src/gemini/index.ts +11 -0
  122. package/src/gemini/message-converter.test.ts +616 -0
  123. package/src/gemini/message-converter.ts +140 -0
  124. package/src/gemini/model-catalog-refresh.test.ts +107 -0
  125. package/src/gemini/model-catalog-refresh.ts +92 -0
  126. package/src/gemini/provider-definition.test.ts +70 -0
  127. package/src/gemini/provider-definition.ts +78 -0
  128. package/src/gemini/provider-extended.test.ts +898 -0
  129. package/src/gemini/provider.spec.ts +216 -0
  130. package/src/gemini/provider.ts +279 -0
  131. package/src/gemini/request-converter.ts +226 -0
  132. package/src/gemini/tool-schema-converter.ts +78 -0
  133. package/src/gemini/types/api-types.ts +235 -0
  134. package/src/gemini/types.ts +121 -0
  135. package/src/gemma/index.ts +5 -0
  136. package/src/gemma/message-factory.ts +38 -0
  137. package/src/gemma/provider-definition.test.ts +43 -0
  138. package/src/gemma/provider-definition.ts +84 -0
  139. package/src/gemma/provider-projection.ts +49 -0
  140. package/src/gemma/provider.test.ts +628 -0
  141. package/src/gemma/provider.ts +308 -0
  142. package/src/gemma/pseudo-command-envelope.ts +58 -0
  143. package/src/gemma/pseudo-tool-call-projector.ts +243 -0
  144. package/src/gemma/pseudo-tool-call-tag-parser.ts +153 -0
  145. package/src/gemma/pseudo-tool-call-types.ts +31 -0
  146. package/src/gemma/reasoning-projector.test.ts +52 -0
  147. package/src/gemma/reasoning-projector.ts +144 -0
  148. package/src/gemma/streaming-projection.ts +79 -0
  149. package/src/gemma/tool-call-argument-parser.ts +126 -0
  150. package/src/gemma/tool-call-projector.test.ts +227 -0
  151. package/src/gemma/tool-call-projector.ts +264 -0
  152. package/src/gemma/types.ts +27 -0
  153. package/src/google/index.ts +11 -0
  154. package/src/google/provider-compat.test.ts +19 -0
  155. package/src/google/provider-definition.ts +6 -0
  156. package/src/google/provider.ts +10 -0
  157. package/src/google/types.ts +5 -0
  158. package/src/index.ts +9 -0
  159. package/src/openai/adapter.test.ts +494 -0
  160. package/src/openai/adapter.ts +145 -0
  161. package/src/openai/chat-completions-chat.ts +189 -0
  162. package/src/openai/executor-integration.test.ts +206 -0
  163. package/src/openai/index.ts +21 -0
  164. package/src/openai/interfaces/payload-logger.ts +48 -0
  165. package/src/openai/loggers/console-payload-logger.test.ts +173 -0
  166. package/src/openai/loggers/console-payload-logger.ts +94 -0
  167. package/src/openai/loggers/console.ts +9 -0
  168. package/src/openai/loggers/file-payload-logger.test.ts +238 -0
  169. package/src/openai/loggers/file-payload-logger.ts +112 -0
  170. package/src/openai/loggers/file.ts +9 -0
  171. package/src/openai/loggers/index.ts +12 -0
  172. package/src/openai/loggers/sanitize-openai-log-data.test.ts +89 -0
  173. package/src/openai/loggers/sanitize-openai-log-data.ts +14 -0
  174. package/src/openai/message-converter.ts +22 -0
  175. package/src/openai/model-catalog-refresh.test.ts +92 -0
  176. package/src/openai/model-catalog-refresh.ts +115 -0
  177. package/src/openai/openai-request-format.ts +92 -0
  178. package/src/openai/parsers/response-parser.test.ts +407 -0
  179. package/src/openai/parsers/response-parser.ts +47 -0
  180. package/src/openai/provider-definition.test.ts +75 -0
  181. package/src/openai/provider-definition.ts +132 -0
  182. package/src/openai/provider.test.ts +1402 -0
  183. package/src/openai/provider.ts +237 -0
  184. package/src/openai/responses-chat.ts +258 -0
  185. package/src/openai/responses-converter.ts +112 -0
  186. package/src/openai/responses-parser.ts +285 -0
  187. package/src/openai/responses-stream-utils.ts +45 -0
  188. package/src/openai/responses-types.ts +195 -0
  189. package/src/openai/streaming/stream-assembler.ts +3 -0
  190. package/src/openai/streaming/stream-handler.test.ts +367 -0
  191. package/src/openai/streaming/stream-handler.ts +119 -0
  192. package/src/openai/types/api-types.ts +112 -0
  193. package/src/openai/types.ts +194 -0
  194. package/src/qwen/defaults.ts +26 -0
  195. package/src/qwen/index.ts +5 -0
  196. package/src/qwen/model-catalog-refresh.test.ts +91 -0
  197. package/src/qwen/model-catalog-refresh.ts +97 -0
  198. package/src/qwen/provider-capabilities.ts +34 -0
  199. package/src/qwen/provider-definition.test.ts +139 -0
  200. package/src/qwen/provider-definition.ts +173 -0
  201. package/src/qwen/provider-streaming-assembly.ts +40 -0
  202. package/src/qwen/provider.test.ts +640 -0
  203. package/src/qwen/provider.ts +293 -0
  204. package/src/qwen/responses-chat.ts +194 -0
  205. package/src/qwen/responses-converter.ts +104 -0
  206. package/src/qwen/responses-parser.ts +299 -0
  207. package/src/qwen/responses-stream-utils.ts +38 -0
  208. package/src/qwen/types.ts +228 -0
  209. package/src/shared/openai-compatible/endpoint-probe.test.ts +52 -0
  210. package/src/shared/openai-compatible/endpoint-probe.ts +43 -0
  211. package/src/shared/openai-compatible/index.ts +6 -0
  212. package/src/shared/openai-compatible/message-converter.test.ts +111 -0
  213. package/src/shared/openai-compatible/message-converter.ts +84 -0
  214. package/src/shared/openai-compatible/native-payload-observer.test.ts +43 -0
  215. package/src/shared/openai-compatible/native-payload-observer.ts +26 -0
  216. package/src/shared/openai-compatible/response-parser.test.ts +172 -0
  217. package/src/shared/openai-compatible/response-parser.ts +180 -0
  218. package/src/shared/openai-compatible/stream-assembler.test.ts +266 -0
  219. package/src/shared/openai-compatible/stream-assembler.ts +248 -0
  220. package/src/shared/openai-compatible/types.ts +59 -0
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Tests for abort signal behavior during streaming.
3
+ * Verifies that ESC (AbortSignal) stops event processing promptly.
4
+ */
5
+
6
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
7
+ import type { TUniversalMessage } from '@robota-sdk/agent-core';
8
+
9
+ vi.mock('@anthropic-ai/sdk', () => {
10
+ const MockAnthropic = vi.fn().mockImplementation(() => ({
11
+ messages: { create: vi.fn() },
12
+ }));
13
+ return { default: MockAnthropic };
14
+ });
15
+
16
+ import Anthropic from '@anthropic-ai/sdk';
17
+ import { AnthropicProvider } from '../provider';
18
+
19
+ /**
20
+ * Create a stream with N text_delta events.
21
+ * onEventYielded fires after each event is consumed by the iterator.
22
+ */
23
+ function makeDeltaStream(
24
+ chunks: string[],
25
+ onEventYielded?: (index: number) => void,
26
+ ): AsyncIterable<Record<string, unknown>> {
27
+ const events: Array<Record<string, unknown>> = [
28
+ {
29
+ type: 'message_start',
30
+ message: {
31
+ usage: { input_tokens: 100, output_tokens: 0 },
32
+ model: 'claude-3-opus-20240229',
33
+ },
34
+ },
35
+ { type: 'content_block_start', index: 0, content_block: { type: 'text' } },
36
+ ];
37
+ for (const chunk of chunks) {
38
+ events.push({
39
+ type: 'content_block_delta',
40
+ index: 0,
41
+ delta: { type: 'text_delta', text: chunk },
42
+ });
43
+ }
44
+ events.push({ type: 'content_block_stop', index: 0 });
45
+ events.push({
46
+ type: 'message_delta',
47
+ delta: { stop_reason: 'end_turn' },
48
+ usage: { output_tokens: 50 },
49
+ });
50
+
51
+ let idx = 0;
52
+ return {
53
+ [Symbol.asyncIterator]() {
54
+ return {
55
+ next() {
56
+ if (idx < events.length) {
57
+ const event = events[idx];
58
+ onEventYielded?.(idx);
59
+ idx++;
60
+ return Promise.resolve({ value: event, done: false });
61
+ }
62
+ return Promise.resolve({ value: undefined, done: true });
63
+ },
64
+ };
65
+ },
66
+ } as AsyncIterable<Record<string, unknown>>;
67
+ }
68
+
69
+ describe('AnthropicProvider abort streaming', () => {
70
+ let provider: AnthropicProvider;
71
+ let mockClient: { messages: { create: ReturnType<typeof vi.fn> } };
72
+
73
+ beforeEach(() => {
74
+ vi.clearAllMocks();
75
+ mockClient = { messages: { create: vi.fn() } };
76
+ provider = new AnthropicProvider({
77
+ client: mockClient as unknown as Anthropic,
78
+ });
79
+ });
80
+
81
+ const userMsg: TUniversalMessage[] = [
82
+ { id: '1', role: 'user', content: 'test', state: 'complete', timestamp: new Date() },
83
+ ];
84
+
85
+ it('stops processing after signal.aborted and returns partial content', async () => {
86
+ const controller = new AbortController();
87
+ const chunks = ['A', 'B', 'C', 'D', 'E'];
88
+
89
+ // Event indices: 0=msg_start, 1=block_start, 2=A_delta, 3=B_delta, 4=C_delta, 5=D_delta, 6=E_delta
90
+ const stream = makeDeltaStream(chunks, (idx) => {
91
+ // Abort after processing B (idx 3 = B_delta yielded)
92
+ if (idx === 3) controller.abort();
93
+ });
94
+ mockClient.messages.create.mockResolvedValue(stream);
95
+
96
+ const result = await provider.chat(userMsg, {
97
+ signal: controller.signal,
98
+ model: 'claude-3-opus-20240229',
99
+ });
100
+
101
+ const content = result.content as string;
102
+ // A was processed before abort. B triggered abort on yield — may or may not be included.
103
+ expect(content).toContain('A');
104
+ // D and E definitely not processed (after abort)
105
+ expect(content).not.toContain('D');
106
+ expect(content).not.toContain('E');
107
+ // Partial result returned
108
+ expect(result.metadata?.stopReason).toBe('aborted');
109
+ });
110
+
111
+ it('returns metadata with stopReason aborted', async () => {
112
+ const controller = new AbortController();
113
+ // idx: 0=msg_start, 1=block_start, 2=Hello_delta, 3=World_delta
114
+ const stream = makeDeltaStream(['Hello', ' World'], (idx) => {
115
+ if (idx === 2) controller.abort(); // abort when Hello is yielded
116
+ });
117
+ mockClient.messages.create.mockResolvedValue(stream);
118
+
119
+ const result = await provider.chat(userMsg, {
120
+ signal: controller.signal,
121
+ model: 'claude-3-opus-20240229',
122
+ });
123
+ expect(result.metadata?.stopReason).toBe('aborted');
124
+ });
125
+
126
+ it('delivers onTextDelta only for chunks before abort', async () => {
127
+ const controller = new AbortController();
128
+ const chunks = ['X', 'Y', 'Z'];
129
+ const deltas: string[] = [];
130
+
131
+ // idx: 0=msg_start, 1=block_start, 2=X_delta, 3=Y_delta, 4=Z_delta
132
+ const stream = makeDeltaStream(chunks, (idx) => {
133
+ if (idx === 3) controller.abort(); // abort when Y is yielded
134
+ });
135
+ mockClient.messages.create.mockResolvedValue(stream);
136
+
137
+ await provider.chat(userMsg, {
138
+ signal: controller.signal,
139
+ onTextDelta: (d: string) => deltas.push(d),
140
+ model: 'claude-3-opus-20240229',
141
+ });
142
+
143
+ // X was processed. Y triggered abort — may not have been delivered to onTextDelta.
144
+ expect(deltas).toContain('X');
145
+ // Z definitely not delivered
146
+ expect(deltas).not.toContain('Z');
147
+ // Total deltas should be less than all chunks
148
+ expect(deltas.length).toBeLessThan(chunks.length);
149
+ });
150
+
151
+ it('handles already-aborted signal', async () => {
152
+ const controller = new AbortController();
153
+ controller.abort(); // Pre-aborted
154
+
155
+ const stream = makeDeltaStream(['Should', 'Not', 'Appear']);
156
+ mockClient.messages.create.mockResolvedValue(stream);
157
+
158
+ const result = await provider.chat(userMsg, {
159
+ signal: controller.signal,
160
+ model: 'claude-3-opus-20240229',
161
+ });
162
+ const content = result.content as string;
163
+ expect(content).not.toContain('Should');
164
+ expect(content).not.toContain('Appear');
165
+ });
166
+
167
+ it('abort during long streaming stops before all events processed', async () => {
168
+ const controller = new AbortController();
169
+ const chunks = Array.from({ length: 20 }, (_, i) => `chunk${i} `);
170
+ const receivedDeltas: string[] = [];
171
+
172
+ // Abort after 5th text_delta (idx 6 = msg_start + block_start + 5 deltas)
173
+ const stream = makeDeltaStream(chunks, (idx) => {
174
+ if (idx === 6) controller.abort();
175
+ });
176
+ mockClient.messages.create.mockResolvedValue(stream);
177
+
178
+ const result = await provider.chat(userMsg, {
179
+ signal: controller.signal,
180
+ model: 'claude-3-opus-20240229',
181
+ onTextDelta: (d: string) => receivedDeltas.push(d),
182
+ });
183
+
184
+ // Should have processed fewer than all 20 chunks
185
+ expect(receivedDeltas.length).toBeLessThan(20);
186
+ expect(result.metadata?.stopReason).toBe('aborted');
187
+ const content = result.content as string;
188
+ expect(content).not.toContain('chunk19');
189
+ });
190
+
191
+ it('processes all events when no abort signal', async () => {
192
+ const chunks = ['Hello', ' ', 'World'];
193
+ const stream = makeDeltaStream(chunks);
194
+ mockClient.messages.create.mockResolvedValue(stream);
195
+
196
+ const result = await provider.chat(userMsg, { model: 'claude-3-opus-20240229' });
197
+ expect(result.content).toBe('Hello World');
198
+ });
199
+ });
@@ -0,0 +1,92 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { refreshAnthropicModelCatalog } from '../model-catalog-refresh';
3
+
4
+ describe('refreshAnthropicModelCatalog', () => {
5
+ it('maps a live Anthropic /v1/models response into provider model catalog entries', async () => {
6
+ const fetcher = vi.fn(async () => ({
7
+ ok: true,
8
+ status: 200,
9
+ json: async () => ({
10
+ data: [
11
+ { id: 'claude-opus-4-7-20251101', display_name: 'Claude Opus 4.7', type: 'model' },
12
+ { id: 'claude-sonnet-4-6-20251101', display_name: 'Claude Sonnet 4.6', type: 'model' },
13
+ ],
14
+ has_more: false,
15
+ }),
16
+ }));
17
+
18
+ const catalog = await refreshAnthropicModelCatalog({ apiKey: 'test-anthropic-key' }, fetcher);
19
+
20
+ expect(fetcher).toHaveBeenCalledWith('https://api.anthropic.com/v1/models', {
21
+ headers: {
22
+ 'x-api-key': 'test-anthropic-key',
23
+ 'anthropic-version': '2023-06-01',
24
+ },
25
+ });
26
+ expect(catalog.status).toBe('live');
27
+ expect(catalog.entries).toHaveLength(2);
28
+ expect(catalog.entries?.map((e) => e.id)).toEqual([
29
+ 'claude-opus-4-7-20251101',
30
+ 'claude-sonnet-4-6-20251101',
31
+ ]);
32
+ expect(catalog.entries?.[0]).toMatchObject({
33
+ id: 'claude-opus-4-7-20251101',
34
+ displayName: 'Claude Opus 4.7',
35
+ lifecycle: 'active',
36
+ });
37
+ expect(catalog.entries?.[1]).toMatchObject({
38
+ id: 'claude-sonnet-4-6-20251101',
39
+ displayName: 'Claude Sonnet 4.6',
40
+ lifecycle: 'active',
41
+ });
42
+ });
43
+
44
+ it('falls back to id as displayName when display_name is absent', async () => {
45
+ const fetcher = vi.fn(async () => ({
46
+ ok: true,
47
+ status: 200,
48
+ json: async () => ({
49
+ data: [{ id: 'claude-haiku-3-5', type: 'model' }],
50
+ has_more: false,
51
+ }),
52
+ }));
53
+
54
+ const catalog = await refreshAnthropicModelCatalog({}, fetcher);
55
+
56
+ expect(catalog.status).toBe('live');
57
+ expect(catalog.entries?.[0]).toMatchObject({
58
+ id: 'claude-haiku-3-5',
59
+ displayName: 'claude-haiku-3-5',
60
+ });
61
+ });
62
+
63
+ it('returns an unavailable catalog when the HTTP response is not ok', async () => {
64
+ const fetcher = vi.fn(async () => ({
65
+ ok: false,
66
+ status: 401,
67
+ json: async () => ({ data: [] }),
68
+ }));
69
+
70
+ const catalog = await refreshAnthropicModelCatalog({}, fetcher);
71
+
72
+ expect(catalog).toMatchObject({
73
+ status: 'unavailable',
74
+ message: 'Anthropic model refresh failed: HTTP 401',
75
+ });
76
+ expect(catalog.entries).toBeUndefined();
77
+ });
78
+
79
+ it('returns an unavailable catalog when a network error is thrown', async () => {
80
+ const fetcher = vi.fn(async () => {
81
+ throw new Error('fetch failed');
82
+ });
83
+
84
+ const catalog = await refreshAnthropicModelCatalog({}, fetcher);
85
+
86
+ expect(catalog).toMatchObject({
87
+ status: 'unavailable',
88
+ message: 'Anthropic model refresh failed: fetch failed',
89
+ });
90
+ expect(catalog.entries).toBeUndefined();
91
+ });
92
+ });
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { AnthropicProvider } from '../provider.js';
3
+ import { createAnthropicProviderDefinition } from '../provider-definition.js';
4
+
5
+ vi.mock('../provider.js', () => {
6
+ const MockAnthropicProvider = vi.fn().mockImplementation((options: object) => ({
7
+ name: 'anthropic',
8
+ options,
9
+ }));
10
+ return { AnthropicProvider: MockAnthropicProvider };
11
+ });
12
+
13
+ describe('createAnthropicProviderDefinition', () => {
14
+ it('requires an API key credential', () => {
15
+ const definition = createAnthropicProviderDefinition();
16
+
17
+ expect(definition.credentialRequirement).toBeUndefined();
18
+ expect(definition.requiresApiKey).toBe(true);
19
+ expect(definition.setupHelpLinks).toEqual([
20
+ {
21
+ kind: 'api-key',
22
+ label: 'Anthropic API keys',
23
+ url: 'https://platform.claude.com/settings/keys',
24
+ sourceUrl: 'https://platform.claude.com/docs/en/api/overview',
25
+ lastVerifiedAt: '2026-05-08',
26
+ },
27
+ ]);
28
+ });
29
+
30
+ it('creates a provider from an API key', () => {
31
+ const definition = createAnthropicProviderDefinition();
32
+
33
+ definition.createProvider({
34
+ name: 'anthropic',
35
+ model: 'claude-sonnet-4-6',
36
+ apiKey: 'sk-ant-test',
37
+ });
38
+
39
+ expect(AnthropicProvider).toHaveBeenCalledWith({
40
+ apiKey: 'sk-ant-test',
41
+ defaultModel: 'claude-sonnet-4-6',
42
+ });
43
+ });
44
+
45
+ it('rejects missing credentials', () => {
46
+ const definition = createAnthropicProviderDefinition();
47
+
48
+ expect(() =>
49
+ definition.createProvider({
50
+ name: 'anthropic',
51
+ model: 'claude-sonnet-4-6',
52
+ }),
53
+ ).toThrow('Provider anthropic requires apiKey');
54
+ });
55
+ });