anthropic_adapter_test.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. package workflow
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "net/http"
  7. "net/http/httptest"
  8. "strings"
  9. "testing"
  10. )
  11. // ---------- helpers ----------
  12. // anthropicJSONResponse returns a valid non-streaming Anthropic Messages API response body.
  13. func anthropicJSONResponse(id, model, text, stopReason string, inputTok, outputTok int) string {
  14. return fmt.Sprintf(`{
  15. "id": %q,
  16. "type": "message",
  17. "role": "assistant",
  18. "model": %q,
  19. "content": [{"type":"text","text":%q}],
  20. "stop_reason": %q,
  21. "usage": {"input_tokens": %d, "output_tokens": %d}
  22. }`, id, model, text, stopReason, inputTok, outputTok)
  23. }
  24. // anthropicSSEStream builds an Anthropic-format SSE stream from the given text chunks.
  25. func anthropicSSEStream(id, model string, chunks []string, stopReason string, inputTok, outputTok int) string {
  26. var sb strings.Builder
  27. // message_start
  28. sb.WriteString(fmt.Sprintf("event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":%q,\"type\":\"message\",\"role\":\"assistant\",\"model\":%q,\"content\":[],\"stop_reason\":null,\"usage\":{\"input_tokens\":%d,\"output_tokens\":0}}}\n\n", id, model, inputTok))
  29. // content_block_start
  30. sb.WriteString("event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n")
  31. // content_block_delta for each chunk
  32. for _, chunk := range chunks {
  33. escaped, _ := json.Marshal(chunk) // properly escape the string
  34. sb.WriteString(fmt.Sprintf("event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":%s}}\n\n", string(escaped)))
  35. }
  36. // content_block_stop
  37. sb.WriteString("event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\n")
  38. // message_delta
  39. sb.WriteString(fmt.Sprintf("event: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":%q},\"usage\":{\"output_tokens\":%d}}\n\n", stopReason, outputTok))
  40. // message_stop
  41. sb.WriteString("event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n")
  42. return sb.String()
  43. }
  44. // ---------- non-streaming tests ----------
  45. func TestAnthropicAdapter_NonStreaming_Basic(t *testing.T) {
  46. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  47. // Verify request headers
  48. if r.Header.Get("x-api-key") != "test-key" {
  49. t.Errorf("expected x-api-key 'test-key', got %q", r.Header.Get("x-api-key"))
  50. }
  51. if r.Header.Get("anthropic-version") != "2023-06-01" {
  52. t.Errorf("expected anthropic-version '2023-06-01', got %q", r.Header.Get("anthropic-version"))
  53. }
  54. if r.Header.Get("Content-Type") != "application/json" {
  55. t.Errorf("expected Content-Type 'application/json', got %q", r.Header.Get("Content-Type"))
  56. }
  57. // Verify request body
  58. var req anthropicReq
  59. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  60. t.Fatalf("failed to decode request: %v", err)
  61. }
  62. if req.Model != "claude-test" {
  63. t.Errorf("expected model 'claude-test', got %q", req.Model)
  64. }
  65. if req.Stream {
  66. t.Error("expected stream=false for non-streaming call")
  67. }
  68. w.Header().Set("Content-Type", "application/json")
  69. fmt.Fprint(w, anthropicJSONResponse("msg_123", "claude-test", "Hello world", "end_turn", 10, 5))
  70. }))
  71. defer server.Close()
  72. adapter := NewAnthropicAdapter(AnthropicConfig{
  73. APIKey: "test-key",
  74. Model: "claude-test",
  75. BaseURL: server.URL,
  76. })
  77. result, err := adapter.Call(context.Background(), map[string]interface{}{
  78. "messages": []interface{}{
  79. map[string]interface{}{"role": "user", "content": "hi"},
  80. },
  81. }, nil)
  82. if err != nil {
  83. t.Fatalf("Call() error: %v", err)
  84. }
  85. if result["content"] != "Hello world" {
  86. t.Errorf("content: got %q, want 'Hello world'", result["content"])
  87. }
  88. if result["model"] != "claude-test" {
  89. t.Errorf("model: got %q, want 'claude-test'", result["model"])
  90. }
  91. if result["finish_reason"] != "end_turn" {
  92. t.Errorf("finish_reason: got %q, want 'end_turn'", result["finish_reason"])
  93. }
  94. if result["response_id"] != "msg_123" {
  95. t.Errorf("response_id: got %q, want 'msg_123'", result["response_id"])
  96. }
  97. usage, ok := result["usage"].(map[string]interface{})
  98. if !ok {
  99. t.Fatal("missing usage")
  100. }
  101. if usage["prompt_tokens"] != 10 {
  102. t.Errorf("prompt_tokens: got %v, want 10", usage["prompt_tokens"])
  103. }
  104. if usage["completion_tokens"] != 5 {
  105. t.Errorf("completion_tokens: got %v, want 5", usage["completion_tokens"])
  106. }
  107. if usage["total_tokens"] != 15 {
  108. t.Errorf("total_tokens: got %v, want 15", usage["total_tokens"])
  109. }
  110. }
  111. func TestAnthropicAdapter_NonStreaming_SystemPrompt(t *testing.T) {
  112. var captured anthropicReq
  113. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  114. json.NewDecoder(r.Body).Decode(&captured)
  115. w.Header().Set("Content-Type", "application/json")
  116. fmt.Fprint(w, anthropicJSONResponse("msg_1", "claude-test", "ok", "end_turn", 5, 2))
  117. }))
  118. defer server.Close()
  119. adapter := NewAnthropicAdapter(AnthropicConfig{APIKey: "k", BaseURL: server.URL})
  120. _, err := adapter.Call(context.Background(), map[string]interface{}{
  121. "messages": []interface{}{
  122. map[string]interface{}{"role": "system", "content": "Be concise"},
  123. map[string]interface{}{"role": "user", "content": "hi"},
  124. },
  125. }, nil)
  126. if err != nil {
  127. t.Fatalf("Call() error: %v", err)
  128. }
  129. if captured.System != "Be concise" {
  130. t.Errorf("system: got %q, want 'Be concise'", captured.System)
  131. }
  132. if len(captured.Messages) != 1 {
  133. t.Fatalf("expected 1 message (user only), got %d", len(captured.Messages))
  134. }
  135. if captured.Messages[0].Role != "user" {
  136. t.Errorf("expected role 'user', got %q", captured.Messages[0].Role)
  137. }
  138. }
  139. func TestAnthropicAdapter_NonStreaming_PromptParam(t *testing.T) {
  140. var captured anthropicReq
  141. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  142. json.NewDecoder(r.Body).Decode(&captured)
  143. w.Header().Set("Content-Type", "application/json")
  144. fmt.Fprint(w, anthropicJSONResponse("msg_1", "claude-test", "ok", "end_turn", 5, 2))
  145. }))
  146. defer server.Close()
  147. adapter := NewAnthropicAdapter(AnthropicConfig{APIKey: "k", BaseURL: server.URL})
  148. _, err := adapter.Call(context.Background(), map[string]interface{}{
  149. "prompt": "hello there",
  150. }, nil)
  151. if err != nil {
  152. t.Fatalf("Call() error: %v", err)
  153. }
  154. if len(captured.Messages) != 1 || captured.Messages[0].Content != "hello there" {
  155. t.Errorf("expected single user message 'hello there', got %+v", captured.Messages)
  156. }
  157. }
  158. func TestAnthropicAdapter_NonStreaming_ModelOverride(t *testing.T) {
  159. var captured anthropicReq
  160. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  161. json.NewDecoder(r.Body).Decode(&captured)
  162. w.Header().Set("Content-Type", "application/json")
  163. fmt.Fprint(w, anthropicJSONResponse("msg_1", "claude-3-haiku", "ok", "end_turn", 5, 2))
  164. }))
  165. defer server.Close()
  166. adapter := NewAnthropicAdapter(AnthropicConfig{
  167. APIKey: "k",
  168. Model: "claude-default",
  169. BaseURL: server.URL,
  170. })
  171. _, err := adapter.Call(context.Background(), map[string]interface{}{
  172. "model": "claude-3-haiku",
  173. "prompt": "hi",
  174. }, nil)
  175. if err != nil {
  176. t.Fatalf("Call() error: %v", err)
  177. }
  178. if captured.Model != "claude-3-haiku" {
  179. t.Errorf("expected model override 'claude-3-haiku', got %q", captured.Model)
  180. }
  181. }
  182. func TestAnthropicAdapter_NonStreaming_APIError(t *testing.T) {
  183. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  184. w.WriteHeader(http.StatusUnauthorized)
  185. fmt.Fprint(w, `{"error":{"type":"authentication_error","message":"invalid x-api-key"}}`)
  186. }))
  187. defer server.Close()
  188. adapter := NewAnthropicAdapter(AnthropicConfig{APIKey: "bad", BaseURL: server.URL})
  189. _, err := adapter.Call(context.Background(), map[string]interface{}{
  190. "prompt": "hi",
  191. }, nil)
  192. if err == nil {
  193. t.Fatal("expected error for 401 response")
  194. }
  195. llmErr, ok := err.(*LLMError)
  196. if !ok {
  197. t.Fatalf("expected *LLMError, got %T: %v", err, err)
  198. }
  199. if llmErr.StatusCode != 401 {
  200. t.Errorf("status code: got %d, want 401", llmErr.StatusCode)
  201. }
  202. if llmErr.Retryable {
  203. t.Error("expected 401 to be non-retryable")
  204. }
  205. }
  206. func TestAnthropicAdapter_NonStreaming_RateLimitRetryable(t *testing.T) {
  207. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  208. w.WriteHeader(http.StatusTooManyRequests)
  209. fmt.Fprint(w, `{"error":{"type":"rate_limit_error","message":"too many requests"}}`)
  210. }))
  211. defer server.Close()
  212. adapter := NewAnthropicAdapter(AnthropicConfig{APIKey: "k", BaseURL: server.URL})
  213. _, err := adapter.Call(context.Background(), map[string]interface{}{
  214. "prompt": "hi",
  215. }, nil)
  216. if err == nil {
  217. t.Fatal("expected error for 429 response")
  218. }
  219. llmErr, ok := err.(*LLMError)
  220. if !ok {
  221. t.Fatalf("expected *LLMError, got %T", err)
  222. }
  223. if !llmErr.Retryable {
  224. t.Error("expected 429 to be retryable")
  225. }
  226. }
  227. func TestAnthropicAdapter_NonStreaming_MissingMessages(t *testing.T) {
  228. adapter := NewAnthropicAdapter(AnthropicConfig{APIKey: "k"})
  229. _, err := adapter.Call(context.Background(), map[string]interface{}{}, nil)
  230. if err == nil {
  231. t.Fatal("expected error for missing messages/prompt")
  232. }
  233. if !strings.Contains(err.Error(), "must include 'messages' or 'prompt'") {
  234. t.Errorf("unexpected error: %v", err)
  235. }
  236. }
  237. // ---------- streaming tests ----------
  238. func TestAnthropicAdapter_Streaming_Basic(t *testing.T) {
  239. chunks := []string{"He", "llo", " wo", "rld", "!"}
  240. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  241. // Verify stream=true in request
  242. var req anthropicReq
  243. json.NewDecoder(r.Body).Decode(&req)
  244. if !req.Stream {
  245. t.Error("expected stream=true in request body")
  246. }
  247. if r.Header.Get("Accept") != "text/event-stream" {
  248. t.Errorf("expected Accept 'text/event-stream', got %q", r.Header.Get("Accept"))
  249. }
  250. w.Header().Set("Content-Type", "text/event-stream")
  251. w.WriteHeader(http.StatusOK)
  252. fmt.Fprint(w, anthropicSSEStream("msg_s1", "claude-test", chunks, "end_turn", 12, 8))
  253. }))
  254. defer server.Close()
  255. adapter := NewAnthropicAdapter(AnthropicConfig{
  256. APIKey: "test-key",
  257. Model: "claude-test",
  258. BaseURL: server.URL,
  259. })
  260. streamCh := make(chan string, 100)
  261. result, err := adapter.Call(context.Background(), map[string]interface{}{
  262. "stream": true,
  263. "messages": []interface{}{
  264. map[string]interface{}{"role": "user", "content": "say hello world"},
  265. },
  266. }, streamCh)
  267. if err != nil {
  268. t.Fatalf("Call() error: %v", err)
  269. }
  270. // Collect streamed chunks
  271. close(streamCh)
  272. var received []string
  273. for c := range streamCh {
  274. received = append(received, c)
  275. }
  276. if len(received) != len(chunks) {
  277. t.Errorf("expected %d stream chunks, got %d", len(chunks), len(received))
  278. }
  279. for i, want := range chunks {
  280. if i < len(received) && received[i] != want {
  281. t.Errorf("chunk[%d]: got %q, want %q", i, received[i], want)
  282. }
  283. }
  284. // Verify assembled result
  285. if result["content"] != "Hello world!" {
  286. t.Errorf("content: got %q, want 'Hello world!'", result["content"])
  287. }
  288. if result["model"] != "claude-test" {
  289. t.Errorf("model: got %q, want 'claude-test'", result["model"])
  290. }
  291. if result["finish_reason"] != "end_turn" {
  292. t.Errorf("finish_reason: got %q, want 'end_turn'", result["finish_reason"])
  293. }
  294. if result["response_id"] != "msg_s1" {
  295. t.Errorf("response_id: got %q, want 'msg_s1'", result["response_id"])
  296. }
  297. usage := result["usage"].(map[string]interface{})
  298. if usage["prompt_tokens"] != 12 {
  299. t.Errorf("prompt_tokens: got %v, want 12", usage["prompt_tokens"])
  300. }
  301. if usage["completion_tokens"] != 8 {
  302. t.Errorf("completion_tokens: got %v, want 8", usage["completion_tokens"])
  303. }
  304. if usage["total_tokens"] != 20 {
  305. t.Errorf("total_tokens: got %v, want 20", usage["total_tokens"])
  306. }
  307. }
  308. func TestAnthropicAdapter_Streaming_FallsBackWhenNoChannel(t *testing.T) {
  309. // When stream=true but channel is nil, should fall back to non-streaming
  310. var captured anthropicReq
  311. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  312. json.NewDecoder(r.Body).Decode(&captured)
  313. w.Header().Set("Content-Type", "application/json")
  314. fmt.Fprint(w, anthropicJSONResponse("msg_1", "claude-test", "ok", "end_turn", 5, 2))
  315. }))
  316. defer server.Close()
  317. adapter := NewAnthropicAdapter(AnthropicConfig{APIKey: "k", BaseURL: server.URL})
  318. result, err := adapter.Call(context.Background(), map[string]interface{}{
  319. "stream": true,
  320. "prompt": "hi",
  321. }, nil) // nil channel
  322. if err != nil {
  323. t.Fatalf("Call() error: %v", err)
  324. }
  325. if captured.Stream {
  326. t.Error("expected stream=false when channel is nil")
  327. }
  328. if result["content"] != "ok" {
  329. t.Errorf("content: got %v, want 'ok'", result["content"])
  330. }
  331. }
  332. func TestAnthropicAdapter_Streaming_HTTPError(t *testing.T) {
  333. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  334. w.WriteHeader(http.StatusInternalServerError)
  335. fmt.Fprint(w, `{"error":{"type":"api_error","message":"internal server error"}}`)
  336. }))
  337. defer server.Close()
  338. adapter := NewAnthropicAdapter(AnthropicConfig{APIKey: "k", BaseURL: server.URL})
  339. streamCh := make(chan string, 100)
  340. _, err := adapter.Call(context.Background(), map[string]interface{}{
  341. "stream": true,
  342. "prompt": "hi",
  343. }, streamCh)
  344. if err == nil {
  345. t.Fatal("expected error for 500 response")
  346. }
  347. llmErr, ok := err.(*LLMError)
  348. if !ok {
  349. t.Fatalf("expected *LLMError, got %T: %v", err, err)
  350. }
  351. if !llmErr.Retryable {
  352. t.Error("expected 500 to be retryable")
  353. }
  354. }
  355. func TestAnthropicAdapter_Streaming_ErrorEvent(t *testing.T) {
  356. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  357. w.Header().Set("Content-Type", "text/event-stream")
  358. w.WriteHeader(http.StatusOK)
  359. // Send a partial stream then error
  360. fmt.Fprint(w, "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_1\",\"model\":\"claude-test\",\"content\":[],\"usage\":{\"input_tokens\":5}}}\n\n")
  361. fmt.Fprint(w, "event: error\ndata: {\"type\":\"error\",\"error\":{\"type\":\"overloaded_error\",\"message\":\"Overloaded\"}}\n\n")
  362. }))
  363. defer server.Close()
  364. adapter := NewAnthropicAdapter(AnthropicConfig{APIKey: "k", BaseURL: server.URL})
  365. streamCh := make(chan string, 100)
  366. _, err := adapter.Call(context.Background(), map[string]interface{}{
  367. "stream": true,
  368. "prompt": "hi",
  369. }, streamCh)
  370. if err == nil {
  371. t.Fatal("expected error from error SSE event")
  372. }
  373. if !strings.Contains(err.Error(), "Overloaded") {
  374. t.Errorf("expected error to mention 'Overloaded', got: %v", err)
  375. }
  376. }
  377. func TestAnthropicAdapter_Streaming_ContextCancellation(t *testing.T) {
  378. // Simulate a slow stream and cancel the context
  379. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  380. w.Header().Set("Content-Type", "text/event-stream")
  381. w.WriteHeader(http.StatusOK)
  382. // Write message_start and one chunk
  383. fmt.Fprint(w, "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_1\",\"model\":\"claude-test\",\"content\":[],\"usage\":{\"input_tokens\":5}}}\n\n")
  384. fmt.Fprint(w, "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n")
  385. fmt.Fprint(w, "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"He\"}}\n\n")
  386. // Flush to ensure the client receives data
  387. if f, ok := w.(http.Flusher); ok {
  388. f.Flush()
  389. }
  390. // Block indefinitely (simulating slow stream) — context cancel will close connection
  391. <-r.Context().Done()
  392. }))
  393. defer server.Close()
  394. adapter := NewAnthropicAdapter(AnthropicConfig{APIKey: "k", BaseURL: server.URL})
  395. ctx, cancel := context.WithCancel(context.Background())
  396. // Use a zero-buffer channel so the send blocks when context is cancelled
  397. streamCh := make(chan string)
  398. // Read one chunk then cancel
  399. go func() {
  400. <-streamCh // read "He"
  401. cancel()
  402. }()
  403. _, err := adapter.Call(ctx, map[string]interface{}{
  404. "stream": true,
  405. "prompt": "hi",
  406. }, streamCh)
  407. if err == nil {
  408. t.Fatal("expected context cancellation error")
  409. }
  410. }
  411. // ---------- structured JSON output tests ----------
  412. func TestAnthropicAdapter_StructuredJSON(t *testing.T) {
  413. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  414. var req anthropicReq
  415. json.NewDecoder(r.Body).Decode(&req)
  416. // Verify schema instruction was injected into system prompt
  417. if !strings.Contains(req.System, "OUTPUT FORMAT REQUIREMENT") {
  418. t.Error("expected schema instruction in system prompt")
  419. }
  420. if !strings.Contains(req.System, `"name"`) {
  421. t.Error("expected schema properties in system prompt")
  422. }
  423. w.Header().Set("Content-Type", "application/json")
  424. fmt.Fprint(w, anthropicJSONResponse("msg_1", "claude-test", `{"name":"Alice","age":30}`, "end_turn", 10, 5))
  425. }))
  426. defer server.Close()
  427. adapter := NewAnthropicAdapter(AnthropicConfig{APIKey: "k", BaseURL: server.URL})
  428. result, err := adapter.Call(context.Background(), map[string]interface{}{
  429. "prompt": "give me a person",
  430. "output_config": map[string]interface{}{
  431. "format": map[string]interface{}{
  432. "type": "json_schema",
  433. "schema": map[string]interface{}{
  434. "type": "object",
  435. "properties": map[string]interface{}{
  436. "name": map[string]interface{}{"type": "string"},
  437. "age": map[string]interface{}{"type": "number"},
  438. },
  439. },
  440. },
  441. },
  442. }, nil)
  443. if err != nil {
  444. t.Fatalf("Call() error: %v", err)
  445. }
  446. // Content should be parsed JSON, not a string
  447. parsed, ok := result["content"].(map[string]interface{})
  448. if !ok {
  449. t.Fatalf("expected content to be parsed map, got %T: %v", result["content"], result["content"])
  450. }
  451. if parsed["name"] != "Alice" {
  452. t.Errorf("name: got %v, want 'Alice'", parsed["name"])
  453. }
  454. }
  455. func TestAnthropicAdapter_StructuredJSON_WithCodeFences(t *testing.T) {
  456. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  457. w.Header().Set("Content-Type", "application/json")
  458. // LLM wraps in code fences despite instruction
  459. fmt.Fprint(w, anthropicJSONResponse("msg_1", "claude-test", "```json\n{\"value\":42}\n```", "end_turn", 10, 5))
  460. }))
  461. defer server.Close()
  462. adapter := NewAnthropicAdapter(AnthropicConfig{APIKey: "k", BaseURL: server.URL})
  463. result, err := adapter.Call(context.Background(), map[string]interface{}{
  464. "prompt": "give me a number",
  465. "output_config": map[string]interface{}{
  466. "format": map[string]interface{}{"type": "json_schema"},
  467. },
  468. }, nil)
  469. if err != nil {
  470. t.Fatalf("Call() error: %v", err)
  471. }
  472. parsed, ok := result["content"].(map[string]interface{})
  473. if !ok {
  474. t.Fatalf("expected parsed map, got %T", result["content"])
  475. }
  476. if parsed["value"] != float64(42) {
  477. t.Errorf("value: got %v, want 42", parsed["value"])
  478. }
  479. }