oai_test.go

   1package oai
   2
   3import (
   4	"context"
   5	"encoding/json"
   6	"net/http"
   7	"net/http/httptest"
   8	"testing"
   9	"time"
  10
  11	"github.com/sashabaranov/go-openai"
  12	"shelley.exe.dev/llm"
  13)
  14
  15func TestToRoleFromString(t *testing.T) {
  16	tests := []struct {
  17		name     string
  18		role     string
  19		expected llm.MessageRole
  20	}{
  21		{
  22			name:     "assistant role",
  23			role:     "assistant",
  24			expected: llm.MessageRoleAssistant,
  25		},
  26		{
  27			name:     "user role",
  28			role:     "user",
  29			expected: llm.MessageRoleUser,
  30		},
  31		{
  32			name:     "tool role maps to assistant",
  33			role:     "tool",
  34			expected: llm.MessageRoleAssistant,
  35		},
  36		{
  37			name:     "system role maps to assistant",
  38			role:     "system",
  39			expected: llm.MessageRoleAssistant,
  40		},
  41		{
  42			name:     "function role maps to assistant",
  43			role:     "function",
  44			expected: llm.MessageRoleAssistant,
  45		},
  46		{
  47			name:     "unknown role defaults to user",
  48			role:     "unknown",
  49			expected: llm.MessageRoleUser,
  50		},
  51		{
  52			name:     "empty role defaults to user",
  53			role:     "",
  54			expected: llm.MessageRoleUser,
  55		},
  56	}
  57
  58	for _, tt := range tests {
  59		t.Run(tt.name, func(t *testing.T) {
  60			result := toRoleFromString(tt.role)
  61			if result != tt.expected {
  62				t.Errorf("toRoleFromString(%q) = %v, expected %v", tt.role, result, tt.expected)
  63			}
  64		})
  65	}
  66}
  67
  68func TestToStopReason(t *testing.T) {
  69	tests := []struct {
  70		name     string
  71		reason   string
  72		expected llm.StopReason
  73	}{
  74		{
  75			name:     "stop reason",
  76			reason:   "stop",
  77			expected: llm.StopReasonStopSequence,
  78		},
  79		{
  80			name:     "length reason",
  81			reason:   "length",
  82			expected: llm.StopReasonMaxTokens,
  83		},
  84		{
  85			name:     "tool_calls reason",
  86			reason:   "tool_calls",
  87			expected: llm.StopReasonToolUse,
  88		},
  89		{
  90			name:     "function_call reason",
  91			reason:   "function_call",
  92			expected: llm.StopReasonToolUse,
  93		},
  94		{
  95			name:     "content_filter reason",
  96			reason:   "content_filter",
  97			expected: llm.StopReasonStopSequence,
  98		},
  99		{
 100			name:     "unknown reason defaults to stop_sequence",
 101			reason:   "unknown",
 102			expected: llm.StopReasonStopSequence,
 103		},
 104		{
 105			name:     "empty reason defaults to stop_sequence",
 106			reason:   "",
 107			expected: llm.StopReasonStopSequence,
 108		},
 109	}
 110
 111	for _, tt := range tests {
 112		t.Run(tt.name, func(t *testing.T) {
 113			result := toStopReason(tt.reason)
 114			if result != tt.expected {
 115				t.Errorf("toStopReason(%q) = %v, expected %v", tt.reason, result, tt.expected)
 116			}
 117		})
 118	}
 119}
 120
 121func TestTokenContextWindow(t *testing.T) {
 122	tests := []struct {
 123		name     string
 124		model    Model
 125		expected int
 126	}{
 127		{
 128			name:     "GPT-4.1 model",
 129			model:    GPT41,
 130			expected: 200000,
 131		},
 132		{
 133			name:     "GPT-4o model",
 134			model:    GPT4o,
 135			expected: 128000,
 136		},
 137		{
 138			name:     "GPT-4o Mini model",
 139			model:    GPT4oMini,
 140			expected: 128000,
 141		},
 142		{
 143			name:     "O3 model",
 144			model:    O3,
 145			expected: 200000,
 146		},
 147		{
 148			name:     "O4-mini model",
 149			model:    O4Mini,
 150			expected: 128000, // o4-mini-2025-04-16 is not in the special cases, so it defaults to 128k
 151		},
 152		{
 153			name:     "Gemini 2.5 Flash model",
 154			model:    Gemini25Flash,
 155			expected: 128000,
 156		},
 157		{
 158			name:     "Gemini 2.5 Pro model",
 159			model:    Gemini25Pro,
 160			expected: 128000,
 161		},
 162		{
 163			name:     "Together Deepseek V3 model",
 164			model:    TogetherDeepseekV3,
 165			expected: 128000,
 166		},
 167		{
 168			name:     "Together Qwen3 model",
 169			model:    TogetherQwen3,
 170			expected: 128000, // Qwen/Qwen3-235B-A22B-fp8-tput is not in the special cases, so it defaults to 128k
 171		},
 172		{
 173			name:     "Default model for unknown",
 174			model:    Model{ModelName: "unknown-model"},
 175			expected: 128000,
 176		},
 177	}
 178
 179	for _, tt := range tests {
 180		t.Run(tt.name, func(t *testing.T) {
 181			service := &Service{Model: tt.model}
 182			result := service.TokenContextWindow()
 183			if result != tt.expected {
 184				t.Errorf("TokenContextWindow() for model %s = %d, expected %d", tt.model.ModelName, result, tt.expected)
 185			}
 186		})
 187	}
 188}
 189
 190func TestMaxImageDimension(t *testing.T) {
 191	// Test both Service and ResponsesService
 192	model := GPT41
 193
 194	// Test Service.MaxImageDimension
 195	service := &Service{Model: model}
 196	result := service.MaxImageDimension()
 197	if result != 0 {
 198		t.Errorf("Service.MaxImageDimension() = %d, expected 0", result)
 199	}
 200
 201	// Test ResponsesService.MaxImageDimension
 202	responsesService := &ResponsesService{Model: model}
 203	result2 := responsesService.MaxImageDimension()
 204	if result2 != 0 {
 205		t.Errorf("ResponsesService.MaxImageDimension() = %d, expected 0", result2)
 206	}
 207}
 208
 209func TestUseSimplifiedPatch(t *testing.T) {
 210	// Test Service.UseSimplifiedPatch
 211	tests := []struct {
 212		name     string
 213		model    Model
 214		expected bool
 215	}{
 216		{
 217			name:     "Default model (false)",
 218			model:    GPT41,
 219			expected: false,
 220		},
 221		{
 222			name:     "Model with UseSimplifiedPatch=true",
 223			model:    Model{UseSimplifiedPatch: true},
 224			expected: true,
 225		},
 226	}
 227
 228	for _, tt := range tests {
 229		t.Run(tt.name, func(t *testing.T) {
 230			service := &Service{Model: tt.model}
 231			result := service.UseSimplifiedPatch()
 232			if result != tt.expected {
 233				t.Errorf("Service.UseSimplifiedPatch() = %v, expected %v", result, tt.expected)
 234			}
 235		})
 236	}
 237}
 238
 239func TestConfigDetails(t *testing.T) {
 240	model := GPT41
 241	service := &Service{Model: model}
 242
 243	details := service.ConfigDetails()
 244
 245	expectedKeys := []string{"base_url", "model_name", "full_url", "api_key_env", "has_api_key_set"}
 246	for _, key := range expectedKeys {
 247		if _, exists := details[key]; !exists {
 248			t.Errorf("ConfigDetails() missing key: %s", key)
 249		}
 250	}
 251
 252	if details["model_name"] != model.ModelName {
 253		t.Errorf("ConfigDetails()[model_name] = %s, expected %s", details["model_name"], model.ModelName)
 254	}
 255
 256	if details["base_url"] != model.URL {
 257		t.Errorf("ConfigDetails()[base_url] = %s, expected %s", details["base_url"], model.URL)
 258	}
 259
 260	if details["api_key_env"] != model.APIKeyEnv {
 261		t.Errorf("ConfigDetails()[api_key_env] = %s, expected %s", details["api_key_env"], model.APIKeyEnv)
 262	}
 263}
 264
 265func TestOAIResponsesServiceUseSimplifiedPatch(t *testing.T) {
 266	model := Model{UseSimplifiedPatch: true}
 267	service := &ResponsesService{Model: model}
 268
 269	result := service.UseSimplifiedPatch()
 270	if !result {
 271		t.Errorf("ResponsesService.UseSimplifiedPatch() = %v, expected true", result)
 272	}
 273}
 274
 275func TestOAIResponsesServiceConfigDetails(t *testing.T) {
 276	model := GPT41
 277	service := &ResponsesService{Model: model}
 278
 279	details := service.ConfigDetails()
 280
 281	expectedKeys := []string{"base_url", "model_name", "full_url", "api_key_env", "has_api_key_set"}
 282	for _, key := range expectedKeys {
 283		if _, exists := details[key]; !exists {
 284			t.Errorf("ConfigDetails() missing key: %s", key)
 285		}
 286	}
 287
 288	// Check that the full_url is different (should be /responses instead of /chat/completions)
 289	if details["full_url"] != model.URL+"/responses" {
 290		t.Errorf("ConfigDetails()[full_url] = %s, expected %s", details["full_url"], model.URL+"/responses")
 291	}
 292}
 293
 294func TestFromLLMContent(t *testing.T) {
 295	// Test text content
 296	textContent := llm.Content{
 297		Type: llm.ContentTypeText,
 298		Text: "Hello, world!",
 299	}
 300	text, toolCalls := fromLLMContent(textContent)
 301	if text != "Hello, world!" {
 302		t.Errorf("fromLLMContent(text) text = %q, expected %q", text, "Hello, world!")
 303	}
 304	if len(toolCalls) != 0 {
 305		t.Errorf("fromLLMContent(text) toolCalls length = %d, expected 0", len(toolCalls))
 306	}
 307
 308	// Test tool use content
 309	toolUseContent := llm.Content{
 310		Type:      llm.ContentTypeToolUse,
 311		ID:        "tool-call-1",
 312		ToolName:  "get_weather",
 313		ToolInput: json.RawMessage(`{"location": "New York"}`),
 314	}
 315	text, toolCalls = fromLLMContent(toolUseContent)
 316	if text != "" {
 317		t.Errorf("fromLLMContent(toolUse) text = %q, expected empty string", text)
 318	}
 319	if len(toolCalls) != 1 {
 320		t.Errorf("fromLLMContent(toolUse) toolCalls length = %d, expected 1", len(toolCalls))
 321	} else {
 322		tc := toolCalls[0]
 323		if tc.Type != openai.ToolTypeFunction {
 324			t.Errorf("toolCall.Type = %q, expected %q", tc.Type, openai.ToolTypeFunction)
 325		}
 326		if tc.ID != "tool-call-1" {
 327			t.Errorf("toolCall.ID = %q, expected %q", tc.ID, "tool-call-1")
 328		}
 329		if tc.Function.Name != "get_weather" {
 330			t.Errorf("toolCall.Function.Name = %q, expected %q", tc.Function.Name, "get_weather")
 331		}
 332		if tc.Function.Arguments != `{"location": "New York"}` {
 333			t.Errorf("toolCall.Function.Arguments = %q, expected %q", tc.Function.Arguments, `{"location": "New York"}`)
 334		}
 335	}
 336
 337	// Test tool result content
 338	toolResultContent := llm.Content{
 339		Type: llm.ContentTypeToolResult,
 340		ToolResult: []llm.Content{
 341			{Type: llm.ContentTypeText, Text: "Sunny"},
 342			{Type: llm.ContentTypeText, Text: "72°F"},
 343		},
 344	}
 345	text, toolCalls = fromLLMContent(toolResultContent)
 346	expectedText := "Sunny\n72°F"
 347	if text != expectedText {
 348		t.Errorf("fromLLMContent(toolResult) text = %q, expected %q", text, expectedText)
 349	}
 350	if len(toolCalls) != 0 {
 351		t.Errorf("fromLLMContent(toolResult) toolCalls length = %d, expected 0", len(toolCalls))
 352	}
 353
 354	// Test default case (thinking content)
 355	thinkingContent := llm.Content{
 356		Type: llm.ContentTypeThinking,
 357		Text: "Thinking about the answer...",
 358	}
 359	text, toolCalls = fromLLMContent(thinkingContent)
 360	if text != "Thinking about the answer..." {
 361		t.Errorf("fromLLMContent(thinking) text = %q, expected %q", text, "Thinking about the answer...")
 362	}
 363	if len(toolCalls) != 0 {
 364		t.Errorf("fromLLMContent(thinking) toolCalls length = %d, expected 0", len(toolCalls))
 365	}
 366}
 367
 368func TestToRawLLMContent(t *testing.T) {
 369	content := toRawLLMContent("test text")
 370	if content.Type != llm.ContentTypeText {
 371		t.Errorf("toRawLLMContent().Type = %v, expected %v", content.Type, llm.ContentTypeText)
 372	}
 373	if content.Text != "test text" {
 374		t.Errorf("toRawLLMContent().Text = %q, expected %q", content.Text, "test text")
 375	}
 376}
 377
 378func TestToToolCallLLMContent(t *testing.T) {
 379	// Test with ID
 380	toolCall := openai.ToolCall{
 381		ID:   "tool-call-1",
 382		Type: openai.ToolTypeFunction,
 383		Function: openai.FunctionCall{
 384			Name:      "get_weather",
 385			Arguments: `{"location": "New York"}`,
 386		},
 387	}
 388	content := toToolCallLLMContent(toolCall)
 389	if content.Type != llm.ContentTypeToolUse {
 390		t.Errorf("toToolCallLLMContent().Type = %v, expected %v", content.Type, llm.ContentTypeToolUse)
 391	}
 392	if content.ID != "tool-call-1" {
 393		t.Errorf("toToolCallLLMContent().ID = %q, expected %q", content.ID, "tool-call-1")
 394	}
 395	if content.ToolName != "get_weather" {
 396		t.Errorf("toToolCallLLMContent().ToolName = %q, expected %q", content.ToolName, "get_weather")
 397	}
 398	if string(content.ToolInput) != `{"location": "New York"}` {
 399		t.Errorf("toToolCallLLMContent().ToolInput = %q, expected %q", string(content.ToolInput), `{"location": "New York"}`)
 400	}
 401
 402	// Test without ID (should generate one)
 403	toolCallNoID := openai.ToolCall{
 404		Type: openai.ToolTypeFunction,
 405		Function: openai.FunctionCall{
 406			Name:      "get_weather",
 407			Arguments: `{"location": "New York"}`,
 408		},
 409	}
 410	contentNoID := toToolCallLLMContent(toolCallNoID)
 411	if contentNoID.ID != "tc_get_weather" {
 412		t.Errorf("toToolCallLLMContent() with no ID = %q, expected %q", contentNoID.ID, "tc_get_weather")
 413	}
 414}
 415
 416func TestToToolResultLLMContent(t *testing.T) {
 417	msg := openai.ChatCompletionMessage{
 418		Role:       "tool",
 419		Content:    "Sunny weather",
 420		ToolCallID: "tool-call-1",
 421	}
 422	content := toToolResultLLMContent(msg)
 423	if content.Type != llm.ContentTypeToolResult {
 424		t.Errorf("toToolResultLLMContent().Type = %v, expected %v", content.Type, llm.ContentTypeToolResult)
 425	}
 426	if content.ToolUseID != "tool-call-1" {
 427		t.Errorf("toToolResultLLMContent().ToolUseID = %q, expected %q", content.ToolUseID, "tool-call-1")
 428	}
 429	if len(content.ToolResult) != 1 {
 430		t.Errorf("toToolResultLLMContent().ToolResult length = %d, expected 1", len(content.ToolResult))
 431	} else {
 432		result := content.ToolResult[0]
 433		if result.Type != llm.ContentTypeText {
 434			t.Errorf("ToolResult[0].Type = %v, expected %v", result.Type, llm.ContentTypeText)
 435		}
 436		if result.Text != "Sunny weather" {
 437			t.Errorf("ToolResult[0].Text = %q, expected %q", result.Text, "Sunny weather")
 438		}
 439	}
 440	if content.ToolError != false {
 441		t.Errorf("toToolResultLLMContent().ToolError = %v, expected false", content.ToolError)
 442	}
 443}
 444
 445func TestToLLMContents(t *testing.T) {
 446	// Test tool response message
 447	toolMsg := openai.ChatCompletionMessage{
 448		Role:       "tool",
 449		Content:    "Sunny weather",
 450		ToolCallID: "tool-call-1",
 451	}
 452	contents := toLLMContents(toolMsg)
 453	if len(contents) != 1 {
 454		t.Errorf("toLLMContents(toolMsg) length = %d, expected 1", len(contents))
 455	} else {
 456		content := contents[0]
 457		if content.Type != llm.ContentTypeToolResult {
 458			t.Errorf("toLLMContents(toolMsg)[0].Type = %v, expected %v", content.Type, llm.ContentTypeToolResult)
 459		}
 460	}
 461
 462	// Test regular message with text
 463	textMsg := openai.ChatCompletionMessage{
 464		Role:    "assistant",
 465		Content: "Hello, world!",
 466	}
 467	contents = toLLMContents(textMsg)
 468	if len(contents) != 1 {
 469		t.Errorf("toLLMContents(textMsg) length = %d, expected 1", len(contents))
 470	} else {
 471		content := contents[0]
 472		if content.Type != llm.ContentTypeText {
 473			t.Errorf("toLLMContents(textMsg)[0].Type = %v, expected %v", content.Type, llm.ContentTypeText)
 474		}
 475		if content.Text != "Hello, world!" {
 476			t.Errorf("toLLMContents(textMsg)[0].Text = %q, expected %q", content.Text, "Hello, world!")
 477		}
 478	}
 479
 480	// Test message with tool calls
 481	toolCallMsg := openai.ChatCompletionMessage{
 482		Role:    "assistant",
 483		Content: "",
 484		ToolCalls: []openai.ToolCall{
 485			{
 486				ID:   "tool-call-1",
 487				Type: openai.ToolTypeFunction,
 488				Function: openai.FunctionCall{
 489					Name:      "get_weather",
 490					Arguments: `{"location": "New York"}`,
 491				},
 492			},
 493		},
 494	}
 495	contents = toLLMContents(toolCallMsg)
 496	if len(contents) != 1 {
 497		t.Errorf("toLLMContents(toolCallMsg) length = %d, expected 1", len(contents))
 498	} else {
 499		content := contents[0]
 500		if content.Type != llm.ContentTypeToolUse {
 501			t.Errorf("toLLMContents(toolCallMsg)[0].Type = %v, expected %v", content.Type, llm.ContentTypeToolUse)
 502		}
 503	}
 504
 505	// Test empty message
 506	emptyMsg := openai.ChatCompletionMessage{
 507		Role:    "assistant",
 508		Content: "",
 509	}
 510	contents = toLLMContents(emptyMsg)
 511	if len(contents) != 1 {
 512		t.Errorf("toLLMContents(emptyMsg) length = %d, expected 1", len(contents))
 513	} else {
 514		content := contents[0]
 515		if content.Type != llm.ContentTypeText {
 516			t.Errorf("toLLMContents(emptyMsg)[0].Type = %v, expected %v", content.Type, llm.ContentTypeText)
 517		}
 518		if content.Text != "" {
 519			t.Errorf("toLLMContents(emptyMsg)[0].Text = %q, expected empty string", content.Text)
 520		}
 521	}
 522}
 523
 524func TestFromLLMToolChoice(t *testing.T) {
 525	// Test nil tool choice
 526	result := fromLLMToolChoice(nil)
 527	if result != nil {
 528		t.Errorf("fromLLMToolChoice(nil) = %v, expected nil", result)
 529	}
 530
 531	// Test specific tool choice
 532	toolChoice := &llm.ToolChoice{
 533		Type: llm.ToolChoiceTypeTool,
 534		Name: "get_weather",
 535	}
 536	result = fromLLMToolChoice(toolChoice)
 537	if toolChoiceResult, ok := result.(openai.ToolChoice); !ok {
 538		t.Errorf("fromLLMToolChoice(tool) result type = %T, expected openai.ToolChoice", result)
 539	} else {
 540		if toolChoiceResult.Type != openai.ToolTypeFunction {
 541			t.Errorf("ToolChoice.Type = %q, expected %q", toolChoiceResult.Type, openai.ToolTypeFunction)
 542		}
 543		if toolChoiceResult.Function.Name != "get_weather" {
 544			t.Errorf("ToolChoice.Function.Name = %q, expected %q", toolChoiceResult.Function.Name, "get_weather")
 545		}
 546	}
 547
 548	// Test auto tool choice
 549	autoChoice := &llm.ToolChoice{Type: llm.ToolChoiceTypeAuto}
 550	result = fromLLMToolChoice(autoChoice)
 551	if result != "auto" {
 552		t.Errorf("fromLLMToolChoice(auto) = %v, expected %q", result, "auto")
 553	}
 554
 555	// Test any tool choice
 556	anyChoice := &llm.ToolChoice{Type: llm.ToolChoiceTypeAny}
 557	result = fromLLMToolChoice(anyChoice)
 558	if result != "any" {
 559		t.Errorf("fromLLMToolChoice(any) = %v, expected %q", result, "any")
 560	}
 561
 562	// Test none tool choice
 563	noneChoice := &llm.ToolChoice{Type: llm.ToolChoiceTypeNone}
 564	result = fromLLMToolChoice(noneChoice)
 565	if result != "none" {
 566		t.Errorf("fromLLMToolChoice(none) = %v, expected %q", result, "none")
 567	}
 568}
 569
 570func TestFromLLMMessage(t *testing.T) {
 571	// Test regular message with text content
 572	textMsg := llm.Message{
 573		Role: llm.MessageRoleUser,
 574		Content: []llm.Content{
 575			{Type: llm.ContentTypeText, Text: "Hello, world!"},
 576		},
 577	}
 578	messages := fromLLMMessage(textMsg)
 579	if len(messages) != 1 {
 580		t.Errorf("fromLLMMessage(textMsg) length = %d, expected 1", len(messages))
 581	} else {
 582		msg := messages[0]
 583		if msg.Role != "user" {
 584			t.Errorf("message.Role = %q, expected %q", msg.Role, "user")
 585		}
 586		if msg.Content != "Hello, world!" {
 587			t.Errorf("message.Content = %q, expected %q", msg.Content, "Hello, world!")
 588		}
 589		if len(msg.ToolCalls) != 0 {
 590			t.Errorf("message.ToolCalls length = %d, expected 0", len(msg.ToolCalls))
 591		}
 592	}
 593
 594	// Test assistant message with tool use
 595	toolMsg := llm.Message{
 596		Role: llm.MessageRoleAssistant,
 597		Content: []llm.Content{
 598			{
 599				Type:      llm.ContentTypeToolUse,
 600				ID:        "tool-call-1",
 601				ToolName:  "get_weather",
 602				ToolInput: json.RawMessage(`{"location": "New York"}`),
 603			},
 604		},
 605	}
 606	messages = fromLLMMessage(toolMsg)
 607	if len(messages) != 1 {
 608		t.Errorf("fromLLMMessage(toolMsg) length = %d, expected 1", len(messages))
 609	} else {
 610		msg := messages[0]
 611		if msg.Role != "assistant" {
 612			t.Errorf("message.Role = %q, expected %q", msg.Role, "assistant")
 613		}
 614		if msg.Content != "" {
 615			t.Errorf("message.Content = %q, expected empty string", msg.Content)
 616		}
 617		if len(msg.ToolCalls) != 1 {
 618			t.Errorf("message.ToolCalls length = %d, expected 1", len(msg.ToolCalls))
 619		} else {
 620			tc := msg.ToolCalls[0]
 621			if tc.ID != "tool-call-1" {
 622				t.Errorf("toolCall.ID = %q, expected %q", tc.ID, "tool-call-1")
 623			}
 624			if tc.Function.Name != "get_weather" {
 625				t.Errorf("toolCall.Function.Name = %q, expected %q", tc.Function.Name, "get_weather")
 626			}
 627		}
 628	}
 629
 630	// Test message with tool result
 631	toolResultMsg := llm.Message{
 632		Role: llm.MessageRoleUser,
 633		Content: []llm.Content{
 634			{
 635				Type:      llm.ContentTypeToolResult,
 636				ToolUseID: "tool-call-1",
 637				ToolResult: []llm.Content{
 638					{Type: llm.ContentTypeText, Text: "Sunny"},
 639					{Type: llm.ContentTypeText, Text: "72°F"},
 640				},
 641			},
 642		},
 643	}
 644	messages = fromLLMMessage(toolResultMsg)
 645	if len(messages) != 1 {
 646		t.Errorf("fromLLMMessage(toolResultMsg) length = %d, expected 1", len(messages))
 647	} else {
 648		msg := messages[0]
 649		if msg.Role != "tool" {
 650			t.Errorf("message.Role = %q, expected %q", msg.Role, "tool")
 651		}
 652		expectedContent := "Sunny\n72°F"
 653		if msg.Content != expectedContent {
 654			t.Errorf("message.Content = %q, expected %q", msg.Content, expectedContent)
 655		}
 656		if msg.ToolCallID != "tool-call-1" {
 657			t.Errorf("message.ToolCallID = %q, expected %q", msg.ToolCallID, "tool-call-1")
 658		}
 659	}
 660
 661	// Test message with tool result and error
 662	toolResultErrorMsg := llm.Message{
 663		Role: llm.MessageRoleUser,
 664		Content: []llm.Content{
 665			{
 666				Type:      llm.ContentTypeToolResult,
 667				ToolUseID: "tool-call-1",
 668				ToolError: true,
 669				ToolResult: []llm.Content{
 670					{Type: llm.ContentTypeText, Text: "API error"},
 671				},
 672			},
 673		},
 674	}
 675	messages = fromLLMMessage(toolResultErrorMsg)
 676	if len(messages) != 1 {
 677		t.Errorf("fromLLMMessage(toolResultErrorMsg) length = %d, expected 1", len(messages))
 678	} else {
 679		msg := messages[0]
 680		if msg.Role != "tool" {
 681			t.Errorf("message.Role = %q, expected %q", msg.Role, "tool")
 682		}
 683		expectedContent := "error: API error"
 684		if msg.Content != expectedContent {
 685			t.Errorf("message.Content = %q, expected %q", msg.Content, expectedContent)
 686		}
 687		if msg.ToolCallID != "tool-call-1" {
 688			t.Errorf("message.ToolCallID = %q, expected %q", msg.ToolCallID, "tool-call-1")
 689		}
 690	}
 691
 692	// Test message with both regular content and tool result
 693	mixedMsg := llm.Message{
 694		Role: llm.MessageRoleAssistant,
 695		Content: []llm.Content{
 696			{Type: llm.ContentTypeText, Text: "The weather is:"},
 697			{
 698				Type:      llm.ContentTypeToolResult,
 699				ToolUseID: "tool-call-1",
 700				ToolResult: []llm.Content{
 701					{Type: llm.ContentTypeText, Text: "Sunny"},
 702				},
 703			},
 704		},
 705	}
 706	messages = fromLLMMessage(mixedMsg)
 707	if len(messages) != 2 {
 708		t.Errorf("fromLLMMessage(mixedMsg) length = %d, expected 2", len(messages))
 709	} else {
 710		// First message should be the tool result
 711		toolMsg := messages[0]
 712		if toolMsg.Role != "tool" {
 713			t.Errorf("first message.Role = %q, expected %q", toolMsg.Role, "tool")
 714		}
 715		if toolMsg.Content != "Sunny" {
 716			t.Errorf("first message.Content = %q, expected %q", toolMsg.Content, "Sunny")
 717		}
 718
 719		// Second message should be the regular content
 720		regularMsg := messages[1]
 721		if regularMsg.Role != "assistant" {
 722			t.Errorf("second message.Role = %q, expected %q", regularMsg.Role, "assistant")
 723		}
 724		if regularMsg.Content != "The weather is:" {
 725			t.Errorf("second message.Content = %q, expected %q", regularMsg.Content, "The weather is:")
 726		}
 727	}
 728}
 729
 730func TestFromLLMTool(t *testing.T) {
 731	tool := &llm.Tool{
 732		Name:        "get_weather",
 733		Description: "Get the current weather for a location",
 734		InputSchema: json.RawMessage(`{"type": "object", "properties": {"location": {"type": "string"}}}`),
 735	}
 736	openaiTool := fromLLMTool(tool)
 737	if openaiTool.Type != openai.ToolTypeFunction {
 738		t.Errorf("fromLLMTool().Type = %q, expected %q", openaiTool.Type, openai.ToolTypeFunction)
 739	}
 740	if openaiTool.Function.Name != "get_weather" {
 741		t.Errorf("fromLLMTool().Function.Name = %q, expected %q", openaiTool.Function.Name, "get_weather")
 742	}
 743	if openaiTool.Function.Description != "Get the current weather for a location" {
 744		t.Errorf("fromLLMTool().Function.Description = %q, expected %q", openaiTool.Function.Description, "Get the current weather for a location")
 745	}
 746	// Note: Parameters is stored as json.RawMessage (byte slice), so we can't directly compare as string
 747	// The important thing is that it's not nil and was assigned
 748	if openaiTool.Function.Parameters == nil {
 749		t.Errorf("fromLLMTool().Function.Parameters should not be nil")
 750	}
 751}
 752
 753func TestListModels(t *testing.T) {
 754	models := ListModels()
 755	if len(models) == 0 {
 756		t.Errorf("ListModels() returned empty slice")
 757	}
 758	// Check that some known models are in the list
 759	expectedModels := []string{"gpt4.1", "gpt4o", "gpt4o-mini", "o3", "o4-mini"}
 760	for _, expected := range expectedModels {
 761		found := false
 762		for _, model := range models {
 763			if model == expected {
 764				found = true
 765				break
 766			}
 767		}
 768		if !found {
 769			t.Errorf("ListModels() missing expected model: %s", expected)
 770		}
 771	}
 772}
 773
 774func TestModelByUserName(t *testing.T) {
 775	// Test finding an existing model
 776	model := ModelByUserName("gpt4.1")
 777	if model.UserName != "gpt4.1" {
 778		t.Errorf("ModelByUserName(gpt4.1).UserName = %q, expected %q", model.UserName, "gpt4.1")
 779	}
 780
 781	// Test finding a non-existent model
 782	model = ModelByUserName("non-existent")
 783	if !model.IsZero() {
 784		t.Errorf("ModelByUserName(non-existent) should return zero value, got: %+v", model)
 785	}
 786}
 787
 788func TestModelIsZero(t *testing.T) {
 789	// Test zero value
 790	var zeroModel Model
 791	if !zeroModel.IsZero() {
 792		t.Errorf("Model{}.IsZero() = false, expected true")
 793	}
 794
 795	// Test non-zero value
 796	model := GPT41
 797	if model.IsZero() {
 798		t.Errorf("GPT41.IsZero() = true, expected false")
 799	}
 800}
 801
 802func TestToLLMUsage(t *testing.T) {
 803	// Create a service instance
 804	service := &Service{}
 805
 806	// Test usage conversion
 807	openaiUsage := openai.Usage{
 808		PromptTokens:     100,
 809		CompletionTokens: 50,
 810	}
 811	usage := service.toLLMUsage(openaiUsage, nil)
 812	if usage.InputTokens != 100 {
 813		t.Errorf("toLLMUsage().InputTokens = %d, expected 100", usage.InputTokens)
 814	}
 815	if usage.OutputTokens != 50 {
 816		t.Errorf("toLLMUsage().OutputTokens = %d, expected 50", usage.OutputTokens)
 817	}
 818	if usage.CacheReadInputTokens != 0 {
 819		t.Errorf("toLLMUsage().CacheReadInputTokens = %d, expected 0", usage.CacheReadInputTokens)
 820	}
 821
 822	// Test with prompt tokens details
 823	openaiUsageWithDetails := openai.Usage{
 824		PromptTokens:     100,
 825		CompletionTokens: 50,
 826		PromptTokensDetails: &openai.PromptTokensDetails{
 827			CachedTokens: 25,
 828		},
 829	}
 830	usage = service.toLLMUsage(openaiUsageWithDetails, nil)
 831	if usage.InputTokens != 100 {
 832		t.Errorf("toLLMUsage().InputTokens = %d, expected 100", usage.InputTokens)
 833	}
 834	if usage.CacheReadInputTokens != 25 {
 835		t.Errorf("toLLMUsage().CacheReadInputTokens = %d, expected 25", usage.CacheReadInputTokens)
 836	}
 837}
 838
 839func TestToLLMResponse(t *testing.T) {
 840	// Create a service instance
 841	service := &Service{}
 842
 843	// Test response with no choices
 844	emptyResponse := &openai.ChatCompletionResponse{
 845		ID:    "test-id",
 846		Model: "gpt-4.1",
 847	}
 848	response := service.toLLMResponse(emptyResponse)
 849	if response.ID != "test-id" {
 850		t.Errorf("toLLMResponse().ID = %q, expected %q", response.ID, "test-id")
 851	}
 852	if response.Model != "gpt-4.1" {
 853		t.Errorf("toLLMResponse().Model = %q, expected %q", response.Model, "gpt-4.1")
 854	}
 855	if response.Role != llm.MessageRoleAssistant {
 856		t.Errorf("toLLMResponse().Role = %v, expected %v", response.Role, llm.MessageRoleAssistant)
 857	}
 858	if len(response.Content) != 0 {
 859		t.Errorf("toLLMResponse().Content length = %d, expected 0", len(response.Content))
 860	}
 861
 862	// Test response with a choice
 863	choiceResponse := &openai.ChatCompletionResponse{
 864		ID:    "test-id-2",
 865		Model: "gpt-4.1",
 866		Choices: []openai.ChatCompletionChoice{
 867			{
 868				Message: openai.ChatCompletionMessage{
 869					Role:    "assistant",
 870					Content: "Hello, world!",
 871				},
 872				FinishReason: openai.FinishReasonStop,
 873			},
 874		},
 875		Usage: openai.Usage{
 876			PromptTokens:     100,
 877			CompletionTokens: 50,
 878		},
 879	}
 880	response = service.toLLMResponse(choiceResponse)
 881	if response.ID != "test-id-2" {
 882		t.Errorf("toLLMResponse().ID = %q, expected %q", response.ID, "test-id-2")
 883	}
 884	if response.Model != "gpt-4.1" {
 885		t.Errorf("toLLMResponse().Model = %q, expected %q", response.Model, "gpt-4.1")
 886	}
 887	if response.Role != llm.MessageRoleAssistant {
 888		t.Errorf("toLLMResponse().Role = %v, expected %v", response.Role, llm.MessageRoleAssistant)
 889	}
 890	if len(response.Content) != 1 {
 891		t.Errorf("toLLMResponse().Content length = %d, expected 1", len(response.Content))
 892	} else {
 893		content := response.Content[0]
 894		if content.Type != llm.ContentTypeText {
 895			t.Errorf("response.Content[0].Type = %v, expected %v", content.Type, llm.ContentTypeText)
 896		}
 897		if content.Text != "Hello, world!" {
 898			t.Errorf("response.Content[0].Text = %q, expected %q", content.Text, "Hello, world!")
 899		}
 900	}
 901	if response.StopReason != llm.StopReasonStopSequence {
 902		t.Errorf("toLLMResponse().StopReason = %v, expected %v", response.StopReason, llm.StopReasonStopSequence)
 903	}
 904	if response.Usage.InputTokens != 100 {
 905		t.Errorf("toLLMResponse().Usage.InputTokens = %d, expected 100", response.Usage.InputTokens)
 906	}
 907	if response.Usage.OutputTokens != 50 {
 908		t.Errorf("toLLMResponse().Usage.OutputTokens = %d, expected 50", response.Usage.OutputTokens)
 909	}
 910}
 911
 912func TestFromLLMSystem(t *testing.T) {
 913	// Test empty system content
 914	messages := fromLLMSystem(nil)
 915	if messages != nil {
 916		t.Errorf("fromLLMSystem(nil) = %v, expected nil", messages)
 917	}
 918
 919	// Test empty slice
 920	messages = fromLLMSystem([]llm.SystemContent{})
 921	if messages != nil {
 922		t.Errorf("fromLLMSystem([]) = %v, expected nil", messages)
 923	}
 924
 925	// Test single system content
 926	systemContent := []llm.SystemContent{
 927		{Text: "You are a helpful assistant."},
 928	}
 929	messages = fromLLMSystem(systemContent)
 930	if len(messages) != 1 {
 931		t.Errorf("fromLLMSystem(single) length = %d, expected 1", len(messages))
 932	} else {
 933		msg := messages[0]
 934		if msg.Role != "system" {
 935			t.Errorf("message.Role = %q, expected %q", msg.Role, "system")
 936		}
 937		if msg.Content != "You are a helpful assistant." {
 938			t.Errorf("message.Content = %q, expected %q", msg.Content, "You are a helpful assistant.")
 939		}
 940	}
 941
 942	// Test multiple system content
 943	multiSystemContent := []llm.SystemContent{
 944		{Text: "You are a helpful assistant."},
 945		{Text: "Be concise in your responses."},
 946	}
 947	messages = fromLLMSystem(multiSystemContent)
 948	if len(messages) != 1 {
 949		t.Errorf("fromLLMSystem(multiple) length = %d, expected 1", len(messages))
 950	} else {
 951		msg := messages[0]
 952		if msg.Role != "system" {
 953			t.Errorf("message.Role = %q, expected %q", msg.Role, "system")
 954		}
 955		expectedContent := "You are a helpful assistant.\nBe concise in your responses."
 956		if msg.Content != expectedContent {
 957			t.Errorf("message.Content = %q, expected %q", msg.Content, expectedContent)
 958		}
 959	}
 960
 961	// Test system content with empty text
 962	emptySystemContent := []llm.SystemContent{
 963		{Text: ""},
 964		{Text: "You are a helpful assistant."},
 965		{Text: ""},
 966	}
 967	messages = fromLLMSystem(emptySystemContent)
 968	if len(messages) != 1 {
 969		t.Errorf("fromLLMSystem(with empty) length = %d, expected 1", len(messages))
 970	} else {
 971		msg := messages[0]
 972		if msg.Role != "system" {
 973			t.Errorf("message.Role = %q, expected %q", msg.Role, "system")
 974		}
 975		if msg.Content != "You are a helpful assistant." {
 976			t.Errorf("message.Content = %q, expected %q", msg.Content, "You are a helpful assistant.")
 977		}
 978	}
 979
 980	// Test system content with all empty text (should return nil)
 981	allEmptySystemContent := []llm.SystemContent{
 982		{Text: ""},
 983		{Text: ""},
 984		{Text: ""},
 985	}
 986	messages = fromLLMSystem(allEmptySystemContent)
 987	if messages != nil {
 988		t.Errorf("fromLLMSystem(all empty) = %v, expected nil", messages)
 989	}
 990}
 991
 992func TestFromLLMMessageEdgeCases(t *testing.T) {
 993	// Test message with tool results containing empty text
 994	toolResultMsg := llm.Message{
 995		Role: llm.MessageRoleUser,
 996		Content: []llm.Content{
 997			{
 998				Type:      llm.ContentTypeToolResult,
 999				ToolUseID: "tool-call-1",
1000				ToolResult: []llm.Content{
1001					{Type: llm.ContentTypeText, Text: ""},
1002				},
1003			},
1004		},
1005	}
1006	messages := fromLLMMessage(toolResultMsg)
1007	if len(messages) != 1 {
1008		t.Errorf("fromLLMMessage(toolResultMsg) length = %d, expected 1", len(messages))
1009	} else {
1010		msg := messages[0]
1011		if msg.Role != "tool" {
1012			t.Errorf("message.Role = %q, expected %q", msg.Role, "tool")
1013		}
1014		// Should be " " (space) when empty to avoid omitempty issues
1015		if msg.Content != " " {
1016			t.Errorf("message.Content = %q, expected %q", msg.Content, " ")
1017		}
1018		if msg.ToolCallID != "tool-call-1" {
1019			t.Errorf("message.ToolCallID = %q, expected %q", msg.ToolCallID, "tool-call-1")
1020		}
1021	}
1022
1023	// Test message with tool results containing only whitespace
1024	toolResultWhitespaceMsg := llm.Message{
1025		Role: llm.MessageRoleUser,
1026		Content: []llm.Content{
1027			{
1028				Type:      llm.ContentTypeToolResult,
1029				ToolUseID: "tool-call-2",
1030				ToolResult: []llm.Content{
1031					{Type: llm.ContentTypeText, Text: "   \n\t  "},
1032				},
1033			},
1034		},
1035	}
1036	messages = fromLLMMessage(toolResultWhitespaceMsg)
1037	if len(messages) != 1 {
1038		t.Errorf("fromLLMMessage(toolResultWhitespaceMsg) length = %d, expected 1", len(messages))
1039	} else {
1040		msg := messages[0]
1041		if msg.Role != "tool" {
1042			t.Errorf("message.Role = %q, expected %q", msg.Role, "tool")
1043		}
1044		// Should be " " (space) when only whitespace to avoid omitempty issues
1045		if msg.Content != " " {
1046			t.Errorf("message.Content = %q, expected %q", msg.Content, " ")
1047		}
1048		if msg.ToolCallID != "tool-call-2" {
1049			t.Errorf("message.ToolCallID = %q, expected %q", msg.ToolCallID, "tool-call-2")
1050		}
1051	}
1052
1053	// Test message with tool error but empty content
1054	toolErrorEmptyMsg := llm.Message{
1055		Role: llm.MessageRoleUser,
1056		Content: []llm.Content{
1057			{
1058				Type:      llm.ContentTypeToolResult,
1059				ToolUseID: "tool-call-3",
1060				ToolError: true,
1061				ToolResult: []llm.Content{
1062					{Type: llm.ContentTypeText, Text: ""},
1063				},
1064			},
1065		},
1066	}
1067	messages = fromLLMMessage(toolErrorEmptyMsg)
1068	if len(messages) != 1 {
1069		t.Errorf("fromLLMMessage(toolErrorEmptyMsg) length = %d, expected 1", len(messages))
1070	} else {
1071		msg := messages[0]
1072		if msg.Role != "tool" {
1073			t.Errorf("message.Role = %q, expected %q", msg.Role, "tool")
1074		}
1075		expectedContent := "error: tool execution failed"
1076		if msg.Content != expectedContent {
1077			t.Errorf("message.Content = %q, expected %q", msg.Content, expectedContent)
1078		}
1079		if msg.ToolCallID != "tool-call-3" {
1080			t.Errorf("message.ToolCallID = %q, expected %q", msg.ToolCallID, "tool-call-3")
1081		}
1082	}
1083
1084	// Test message with tool error and content
1085	toolErrorWithContentMsg := llm.Message{
1086		Role: llm.MessageRoleUser,
1087		Content: []llm.Content{
1088			{
1089				Type:      llm.ContentTypeToolResult,
1090				ToolUseID: "tool-call-4",
1091				ToolError: true,
1092				ToolResult: []llm.Content{
1093					{Type: llm.ContentTypeText, Text: "something went wrong"},
1094				},
1095			},
1096		},
1097	}
1098	messages = fromLLMMessage(toolErrorWithContentMsg)
1099	if len(messages) != 1 {
1100		t.Errorf("fromLLMMessage(toolErrorWithContentMsg) length = %d, expected 1", len(messages))
1101	} else {
1102		msg := messages[0]
1103		if msg.Role != "tool" {
1104			t.Errorf("message.Role = %q, expected %q", msg.Role, "tool")
1105		}
1106		expectedContent := "error: something went wrong"
1107		if msg.Content != expectedContent {
1108			t.Errorf("message.Content = %q, expected %q", msg.Content, expectedContent)
1109		}
1110		if msg.ToolCallID != "tool-call-4" {
1111			t.Errorf("message.ToolCallID = %q, expected %q", msg.ToolCallID, "tool-call-4")
1112		}
1113	}
1114
1115	// Test message with mixed content (regular text + tool results)
1116	mixedContentMsg := llm.Message{
1117		Role: llm.MessageRoleAssistant,
1118		Content: []llm.Content{
1119			{Type: llm.ContentTypeText, Text: "Here's the result:"},
1120			{
1121				Type:      llm.ContentTypeToolResult,
1122				ToolUseID: "tool-call-5",
1123				ToolResult: []llm.Content{
1124					{Type: llm.ContentTypeText, Text: "The weather is sunny"},
1125				},
1126			},
1127			{Type: llm.ContentTypeText, Text: "Have a nice day!"},
1128		},
1129	}
1130	messages = fromLLMMessage(mixedContentMsg)
1131	// Should produce 2 messages: one tool result message and one regular message
1132	if len(messages) != 2 {
1133		t.Errorf("fromLLMMessage(mixedContentMsg) length = %d, expected 2", len(messages))
1134	} else {
1135		// First message should be the tool result
1136		toolMsg := messages[0]
1137		if toolMsg.Role != "tool" {
1138			t.Errorf("first message.Role = %q, expected %q", toolMsg.Role, "tool")
1139		}
1140		if toolMsg.Content != "The weather is sunny" {
1141			t.Errorf("first message.Content = %q, expected %q", toolMsg.Content, "The weather is sunny")
1142		}
1143		if toolMsg.ToolCallID != "tool-call-5" {
1144			t.Errorf("first message.ToolCallID = %q, expected %q", toolMsg.ToolCallID, "tool-call-5")
1145		}
1146
1147		// Second message should be the regular content
1148		regularMsg := messages[1]
1149		if regularMsg.Role != "assistant" {
1150			t.Errorf("second message.Role = %q, expected %q", regularMsg.Role, "assistant")
1151		}
1152		// Should combine both text contents with newline
1153		expectedContent := "Here's the result:\nHave a nice day!"
1154		if regularMsg.Content != expectedContent {
1155			t.Errorf("second message.Content = %q, expected %q", regularMsg.Content, expectedContent)
1156		}
1157	}
1158}
1159
1160func TestTokenContextWindowAdditionalCases(t *testing.T) {
1161	tests := []struct {
1162		name     string
1163		model    Model
1164		expected int
1165	}{
1166		{
1167			name:     "GPT-4.1 Mini model",
1168			model:    GPT41Mini,
1169			expected: 200000,
1170		},
1171		{
1172			name:     "GPT-4.1 Nano model",
1173			model:    GPT41Nano,
1174			expected: 200000,
1175		},
1176		{
1177			name:     "Qwen3 Coder Fireworks model",
1178			model:    Qwen3CoderFireworks,
1179			expected: 256000,
1180		},
1181		{
1182			name:     "Qwen3 Coder Cerebras model",
1183			model:    Qwen3CoderCerebras,
1184			expected: 128000, // The model name "qwen-3-coder-480b" is not in the special cases, so it defaults to 128k
1185		},
1186		{
1187			name:     "GLM model",
1188			model:    GLM,
1189			expected: 128000,
1190		},
1191		{
1192			name:     "Qwen model",
1193			model:    Qwen,
1194			expected: 256000,
1195		},
1196		{
1197			name:     "GPT-OSS 20B model",
1198			model:    GPTOSS20B,
1199			expected: 128000,
1200		},
1201		{
1202			name:     "GPT-OSS 120B model",
1203			model:    GPTOSS120B,
1204			expected: 128000,
1205		},
1206		{
1207			name:     "GPT-5 model",
1208			model:    GPT5,
1209			expected: 256000,
1210		},
1211		{
1212			name:     "GPT-5 Mini model",
1213			model:    GPT5Mini,
1214			expected: 256000,
1215		},
1216		{
1217			name:     "GPT-5 Nano model",
1218			model:    GPT5Nano,
1219			expected: 256000,
1220		},
1221		{
1222			name:     "Unknown model defaults to 128k",
1223			model:    Model{ModelName: "unknown-model-name"},
1224			expected: 128000,
1225		},
1226	}
1227
1228	for _, tt := range tests {
1229		t.Run(tt.name, func(t *testing.T) {
1230			service := &Service{Model: tt.model}
1231			result := service.TokenContextWindow()
1232			if result != tt.expected {
1233				t.Errorf("TokenContextWindow() for model %s = %d, expected %d", tt.model.ModelName, result, tt.expected)
1234			}
1235		})
1236	}
1237}
1238
1239func TestServiceDo(t *testing.T) {
1240	// Create a mock OpenAI server
1241	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1242		if r.URL.Path != "/v1/chat/completions" {
1243			t.Errorf("Expected path /v1/chat/completions, got %s", r.URL.Path)
1244		}
1245		if r.Header.Get("Authorization") != "Bearer test-api-key" {
1246			t.Errorf("Expected Authorization header, got %s", r.Header.Get("Authorization"))
1247		}
1248
1249		// Send a mock response
1250		response := openai.ChatCompletionResponse{
1251			ID:      "chatcmpl-test123",
1252			Object:  "chat.completion",
1253			Created: time.Now().Unix(),
1254			Model:   "gpt-4.1-2025-04-14",
1255			Choices: []openai.ChatCompletionChoice{
1256				{
1257					Index: 0,
1258					Message: openai.ChatCompletionMessage{
1259						Role:    "assistant",
1260						Content: "Hello! How can I help you today?",
1261					},
1262					FinishReason: "stop",
1263				},
1264			},
1265			Usage: openai.Usage{
1266				PromptTokens:     10,
1267				CompletionTokens: 20,
1268				TotalTokens:      30,
1269			},
1270		}
1271
1272		w.Header().Set("Content-Type", "application/json")
1273		json.NewEncoder(w).Encode(response)
1274	}))
1275	defer server.Close()
1276
1277	// Create a service with the mock server
1278	ctx := context.Background()
1279	svc := &Service{
1280		APIKey:   "test-api-key",
1281		Model:    GPT41,
1282		ModelURL: server.URL + "/v1",
1283	}
1284
1285	// Create a test request
1286	req := &llm.Request{
1287		Messages: []llm.Message{
1288			{
1289				Role: llm.MessageRoleUser,
1290				Content: []llm.Content{
1291					{Type: llm.ContentTypeText, Text: "Hello!"},
1292				},
1293			},
1294		},
1295	}
1296
1297	// Call the Do method
1298	resp, err := svc.Do(ctx, req)
1299	if err != nil {
1300		t.Fatalf("Do() error = %v", err)
1301	}
1302
1303	// Verify the response
1304	if resp == nil {
1305		t.Fatal("Do() returned nil response")
1306	}
1307	if resp.Role != llm.MessageRoleAssistant {
1308		t.Errorf("resp.Role = %v, expected %v", resp.Role, llm.MessageRoleAssistant)
1309	}
1310	if len(resp.Content) != 1 {
1311		t.Errorf("resp.Content length = %d, expected 1", len(resp.Content))
1312	} else {
1313		content := resp.Content[0]
1314		if content.Type != llm.ContentTypeText {
1315			t.Errorf("content.Type = %v, expected %v", content.Type, llm.ContentTypeText)
1316		}
1317		if content.Text != "Hello! How can I help you today?" {
1318			t.Errorf("content.Text = %q, expected %q", content.Text, "Hello! How can I help you today?")
1319		}
1320	}
1321	if resp.Usage.InputTokens != 10 {
1322		t.Errorf("resp.Usage.InputTokens = %d, expected 10", resp.Usage.InputTokens)
1323	}
1324	if resp.Usage.OutputTokens != 20 {
1325		t.Errorf("resp.Usage.OutputTokens = %d, expected 20", resp.Usage.OutputTokens)
1326	}
1327}