internal/llm/capability_llm_client_test.go
Rama main · réplica de lectura del repositorio original.
package llm
import (
"context"
"encoding/json"
"io"
"net/http"
"reflect"
"strings"
"testing"
"time"
"github.com/yeomyeonggeori/blueclaw/internal/capability"
"github.com/yeomyeonggeori/blueprotocol/model"
)
func TestCapabilityLLMClientSendsStructuredRequestWithoutAuthorization(t *testing.T) {
var receivedAuthorization string
var receivedDocument capabilityStructuredResponseRequestDocument
httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
if request.URL.Path != "/v1/llm/structured" {
t.Fatalf("expected structured llm path, got %q", request.URL.Path)
}
receivedAuthorization = request.Header.Get("Authorization")
errorValue := json.NewDecoder(request.Body).Decode(&receivedDocument)
if errorValue != nil {
t.Fatalf("expected request document to decode: %v", errorValue)
}
return jsonCapabilityResponse(http.StatusOK, `{"provider":"capabilityLLM","model":"gemma-4-E4B-it","content":"{\"reply\":\"hello\"}","selectedBackend":"gpu","constraintMode":"openai_json_schema"}`), nil
}}
client := CapabilityLLMClient{
CapabilityClient: capability.Client{
Endpoint: "http://internkim-capability",
HTTPClient: httpClient,
},
ModelName: "gemma-4-E4B-it",
ExecutionMode: "local",
}
structuredResponse, errorValue := client.GenerateStructuredResponse(context.Background(), buildTestStructuredResponseRequest())
if errorValue != nil {
t.Fatalf("expected structured response: %v", errorValue)
}
if receivedAuthorization != "" {
t.Fatalf("expected no authorization header, got %q", receivedAuthorization)
}
if receivedDocument.Model != "gemma-4-E4B-it" {
t.Fatalf("expected model to be passed through, got %q", receivedDocument.Model)
}
if receivedDocument.ExecutionMode != "local" {
t.Fatalf("expected execution mode to be passed through, got %q", receivedDocument.ExecutionMode)
}
if string(receivedDocument.StructuredOutputSchema.Document) != `{"type":"object","properties":{"reply":{"type":"string"}},"required":["reply"],"additionalProperties":false}` {
t.Fatalf("expected schema document to be unchanged, got %s", string(receivedDocument.StructuredOutputSchema.Document))
}
if receivedDocument.GenerationOptions != nil {
t.Fatalf("expected empty generation options to be omitted, got %+v", receivedDocument.GenerationOptions)
}
if structuredResponse.Content != `{"reply":"hello"}` {
t.Fatalf("expected capability content to be returned, got %q", structuredResponse.Content)
}
if structuredResponse.ModelName != "gemma-4-E4B-it" {
t.Fatalf("expected model name from capability response, got %q", structuredResponse.ModelName)
}
if structuredResponse.ConstraintMode != "openai_json_schema" {
t.Fatalf("expected constraint mode from capability response, got %q", structuredResponse.ConstraintMode)
}
}
func TestCapabilityLLMClientRoundTripsUsage(t *testing.T) {
httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
return jsonCapabilityResponse(http.StatusOK, `{"provider":"capabilityLLM","model":"gemma-4-E4B-it","content":"{\"reply\":\"ok\"}","selectedBackend":"gpu","constraintMode":"openai_json_schema","usage":{"promptTokens":123,"completionTokens":45,"totalTokens":168}}`), nil
}}
client := CapabilityLLMClient{
CapabilityClient: capability.Client{
Endpoint: "http://internkim-capability",
HTTPClient: httpClient,
},
ModelName: "gemma-4-E4B-it",
}
structuredResponse, errorValue := client.GenerateStructuredResponse(context.Background(), buildTestStructuredResponseRequest())
if errorValue != nil {
t.Fatalf("expected structured response: %v", errorValue)
}
if structuredResponse.Usage.PromptTokens != 123 {
t.Fatalf("expected prompt tokens 123, got %d", structuredResponse.Usage.PromptTokens)
}
if structuredResponse.Usage.CompletionTokens != 45 {
t.Fatalf("expected completion tokens 45, got %d", structuredResponse.Usage.CompletionTokens)
}
if structuredResponse.Usage.TotalTokens != 168 {
t.Fatalf("expected total tokens 168, got %d", structuredResponse.Usage.TotalTokens)
}
}
func TestCapabilityLLMClientReturnsCapabilityError(t *testing.T) {
httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
return jsonCapabilityResponse(http.StatusBadGateway, "local model unavailable"), nil
}}
client := CapabilityLLMClient{
CapabilityClient: capability.Client{
Endpoint: "http://internkim-capability",
HTTPClient: httpClient,
},
ModelName: "gemma-4-E4B-it",
}
_, errorValue := client.GenerateStructuredResponse(context.Background(), buildTestStructuredResponseRequest())
if errorValue == nil {
t.Fatal("expected capability error")
}
if errorValue.Error() != "local model unavailable" {
t.Fatalf("expected capability error body, got %q", errorValue.Error())
}
}
func TestCapabilityLLMClientForwardsGenerationOptions(t *testing.T) {
seed := int64(1234)
temperature := 0.2
var receivedDocument capabilityStructuredResponseRequestDocument
httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
if errorValue := json.NewDecoder(request.Body).Decode(&receivedDocument); errorValue != nil {
t.Fatalf("expected request document to decode: %v", errorValue)
}
return jsonCapabilityResponse(http.StatusOK, `{"content":"{\"reply\":\"ok\"}"}`), nil
}}
client := CapabilityLLMClient{
CapabilityClient: capability.Client{
Endpoint: "http://internkim-capability",
HTTPClient: httpClient,
},
ModelName: "gemma",
}
request := buildTestStructuredResponseRequest()
request.GenerationOptions = GenerationOptions{Seed: &seed, Temperature: &temperature}
_, errorValue := client.GenerateStructuredResponse(context.Background(), request)
if errorValue != nil {
t.Fatalf("expected structured response: %v", errorValue)
}
if receivedDocument.GenerationOptions == nil || receivedDocument.GenerationOptions.Seed == nil || *receivedDocument.GenerationOptions.Seed != seed {
t.Fatalf("expected seed to be forwarded, got %+v", receivedDocument.GenerationOptions)
}
if receivedDocument.GenerationOptions.Temperature == nil || *receivedDocument.GenerationOptions.Temperature != temperature {
t.Fatalf("expected temperature to be forwarded, got %+v", receivedDocument.GenerationOptions)
}
}
func TestCapabilityLLMClientGenerateResponseUsesTextEndpoint(t *testing.T) {
var receivedDocument capabilityTextResponseRequestDocument
httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
if request.URL.Path != "/v1/llm/text" {
t.Fatalf("expected text llm path, got %q", request.URL.Path)
}
if request.Header.Get("Authorization") != "" {
t.Fatalf("expected no authorization header, got %q", request.Header.Get("Authorization"))
}
if errorValue := json.NewDecoder(request.Body).Decode(&receivedDocument); errorValue != nil {
t.Fatalf("expected request document to decode: %v", errorValue)
}
return jsonCapabilityResponse(http.StatusOK, `{"provider":"capabilityLLM","model":"gemma","content":"plain reply","selectedBackend":"cpu"}`), nil
}}
client := CapabilityLLMClient{
CapabilityClient: capability.Client{
Endpoint: "http://internkim-capability",
HTTPClient: httpClient,
},
ModelName: "gemma",
ExecutionMode: "local",
}
response, errorValue := client.GenerateResponse(context.Background(), "hello")
if errorValue != nil {
t.Fatalf("expected text response: %v", errorValue)
}
if response != "plain reply" {
t.Fatalf("expected plain text response, got %q", response)
}
if receivedDocument.Model != "gemma" || receivedDocument.ExecutionMode != "local" {
t.Fatalf("expected model and execution mode, got %+v", receivedDocument)
}
if len(receivedDocument.Messages) != 1 || receivedDocument.Messages[0].Content != "hello" {
t.Fatalf("expected prompt message, got %+v", receivedDocument.Messages)
}
}
func TestCapabilityLLMClientGenerateChatCompletionSendsNativeToolRequest(t *testing.T) {
seed := int64(42)
temperature := 0.2
maxTokens := 192
var receivedDocument map[string]any
httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
if request.URL.Path != "/v1/llm/chat" {
t.Fatalf("expected chat llm path, got %q", request.URL.Path)
}
if request.Header.Get("Authorization") != "" {
t.Fatalf("expected no authorization header, got %q", request.Header.Get("Authorization"))
}
if errorValue := json.NewDecoder(request.Body).Decode(&receivedDocument); errorValue != nil {
t.Fatalf("expected request document to decode: %v", errorValue)
}
return jsonCapabilityResponse(http.StatusOK, `{"finishReason":"tool_calls","provider":"capabilityLLM","model":"gemma","message":{"role":"assistant","content":"","toolCalls":[{"id":"call-1","type":"function","function":{"name":"lookup","arguments":"{\"query\":\"status\"}"}}]},"usage":{"promptTokens":4,"completionTokens":2,"totalTokens":6}}`), nil
}}
client := CapabilityLLMClient{
CapabilityClient: capability.Client{
Endpoint: "http://internkim-capability",
HTTPClient: httpClient,
},
ModelName: "gemma",
ExecutionMode: "remote",
}
response, errorValue := client.GenerateChatCompletion(context.Background(), ChatCompletionRequest{
Messages: []ChatCompletionMessage{{Role: "user", Content: "check"}},
Tools: []ChatCompletionTool{{
Type: "function",
Function: ChatCompletionFunction{
Name: "lookup",
Description: "Lookup status",
Parameters: json.RawMessage(`{"type":"object","properties":{"query":{"type":"string"}},"required":["query"]}`),
},
}},
ToolChoice: json.RawMessage(`{"type":"function","function":{"name":"lookup"}}`),
ParallelToolCalls: true,
GenerationOptions: GenerationOptions{Seed: &seed, Temperature: &temperature, MaxTokens: &maxTokens},
})
if errorValue != nil {
t.Fatalf("expected chat completion response: %v", errorValue)
}
if receivedDocument["model"] != "gemma" || receivedDocument["executionMode"] != "remote" {
t.Fatalf("expected model and execution mode, got %+v", receivedDocument)
}
tools := receivedDocument["tools"].([]any)
tool := tools[0].(map[string]any)
function := tool["function"].(map[string]any)
if tool["type"] != "function" || function["name"] != "lookup" {
t.Fatalf("expected function tool, got %+v", tool)
}
toolChoice := receivedDocument["toolChoice"].(map[string]any)
toolChoiceFunction := toolChoice["function"].(map[string]any)
if toolChoice["type"] != "function" || toolChoiceFunction["name"] != "lookup" {
t.Fatalf("expected object tool choice, got %+v", toolChoice)
}
if receivedDocument["parallelToolCalls"] != true {
t.Fatalf("expected parallel tool calls to be forwarded, got %+v", receivedDocument)
}
generationOptions := receivedDocument["generationOptions"].(map[string]any)
if generationOptions["seed"] != float64(seed) || generationOptions["temperature"] != temperature || generationOptions["maxTokens"] != float64(maxTokens) {
t.Fatalf("expected generation options to be forwarded, got %+v", generationOptions)
}
if response.FinishReason != "tool_calls" {
t.Fatalf("expected tool_calls finish reason, got %q", response.FinishReason)
}
if len(response.Message.ToolCalls) != 1 {
t.Fatalf("expected one tool call, got %+v", response.Message.ToolCalls)
}
toolCall := response.Message.ToolCalls[0]
if toolCall.Function.Name != "lookup" || toolCall.Function.Arguments != `{"query":"status"}` {
t.Fatalf("expected JSON string arguments, got %+v", toolCall)
}
if response.Usage.TotalTokens != 6 {
t.Fatalf("expected usage to round trip, got %+v", response.Usage)
}
}
func TestCapabilityLLMClientGenerateChatCompletionForwardsStringToolChoice(t *testing.T) {
var receivedDocument map[string]any
httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
if errorValue := json.NewDecoder(request.Body).Decode(&receivedDocument); errorValue != nil {
t.Fatalf("expected request document to decode: %v", errorValue)
}
return jsonCapabilityResponse(http.StatusOK, `{"finishReason":"stop","message":{"role":"assistant","content":"done","toolCalls":[]}}`), nil
}}
client := CapabilityLLMClient{
CapabilityClient: capability.Client{
Endpoint: "http://internkim-capability",
HTTPClient: httpClient,
},
ModelName: "gemma",
}
response, errorValue := client.GenerateChatCompletion(context.Background(), ChatCompletionRequest{
Messages: []ChatCompletionMessage{{Role: "user", Content: "hello"}},
ToolChoice: json.RawMessage(`"auto"`),
ParallelToolCalls: false,
})
if errorValue != nil {
t.Fatalf("expected chat completion response: %v", errorValue)
}
if receivedDocument["toolChoice"] != "auto" {
t.Fatalf("expected string tool choice, got %+v", receivedDocument)
}
if receivedDocument["parallelToolCalls"] != false {
t.Fatalf("expected parallel tool calls false, got %+v", receivedDocument)
}
if response.ProviderName != "capabilityLLM" || response.ModelName != "gemma" {
t.Fatalf("expected default provider and model, got %+v", response)
}
if response.Message.Content != "done" {
t.Fatalf("expected assistant content, got %q", response.Message.Content)
}
}
func TestCapabilityLLMClientSendsRequesterContext(t *testing.T) {
var receivedDocument capabilityStructuredResponseRequestDocument
httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
if errorValue := json.NewDecoder(request.Body).Decode(&receivedDocument); errorValue != nil {
t.Fatalf("expected request document to decode: %v", errorValue)
}
return jsonCapabilityResponse(http.StatusOK, `{"provider":"capabilityLLM","model":"gemma","content":"{\"reply\":\"ok\"}","selectedBackend":"device_local"}`), nil
}}
client := CapabilityLLMClient{
CapabilityClient: capability.Client{
Endpoint: "http://internkim-capability",
HTTPClient: httpClient,
},
ModelName: "gemma",
}
requestContext := RequestContext{
RequesterPersonID: "person-1",
RequesterEmail: "alice@example.com",
RequesterName: "Alice",
RequesterPlatformUserID: "user-1",
ConversationID: "dm:channel-1",
Platform: "mattermost",
}
_, errorValue := client.GenerateStructuredResponse(ContextWithRequestContext(context.Background(), requestContext), buildTestStructuredResponseRequest())
if errorValue != nil {
t.Fatalf("expected structured response: %v", errorValue)
}
if receivedDocument.Context == nil || *receivedDocument.Context != requestContext {
t.Fatalf("expected requester context, got %+v", receivedDocument.Context)
}
}
func TestCapabilityLLMClientRecoveryResponseUsesLocalCapableExecutionMode(t *testing.T) {
receivedExecutionModes := []string{}
httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
var receivedDocument capabilityTextResponseRequestDocument
if errorValue := json.NewDecoder(request.Body).Decode(&receivedDocument); errorValue != nil {
t.Fatalf("expected request document to decode: %v", errorValue)
}
receivedExecutionModes = append(receivedExecutionModes, receivedDocument.ExecutionMode)
if receivedDocument.ExecutionMode == "auto" {
return jsonCapabilityResponse(http.StatusOK, `{"provider":"capabilityLLM","model":"gemma","content":"local-ish reply","selectedBackend":"cpu"}`), nil
}
return jsonCapabilityResponse(http.StatusBadGateway, "remote unavailable"), nil
}}
client := CapabilityLLMClient{
CapabilityClient: capability.Client{
Endpoint: "http://internkim-capability",
HTTPClient: httpClient,
},
ModelName: "gemma",
ExecutionMode: "remote",
}
response, errorValue := client.GenerateRecoveryResponse(context.Background(), "hello")
if errorValue != nil {
t.Fatalf("expected recovery text response: %v", errorValue)
}
if response != "local-ish reply" {
t.Fatalf("expected recovery response, got %q", response)
}
if len(receivedExecutionModes) != 1 || receivedExecutionModes[0] != "auto" {
t.Fatalf("expected recovery to use auto execution mode, got %+v", receivedExecutionModes)
}
}
func TestCapabilityLLMClientRecoveryResponseFallsBackToDeviceAfterAutoFailure(t *testing.T) {
receivedExecutionModes := []string{}
httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
var receivedDocument capabilityTextResponseRequestDocument
if errorValue := json.NewDecoder(request.Body).Decode(&receivedDocument); errorValue != nil {
t.Fatalf("expected request document to decode: %v", errorValue)
}
receivedExecutionModes = append(receivedExecutionModes, receivedDocument.ExecutionMode)
if receivedDocument.ExecutionMode == "auto" {
return jsonCapabilityResponse(http.StatusBadGateway, "remote unavailable"), nil
}
return jsonCapabilityResponse(http.StatusOK, `{"provider":"capabilityLLM","model":"gemma","content":"device recovery reply","selectedBackend":"device"}`), nil
}}
client := CapabilityLLMClient{
CapabilityClient: capability.Client{
Endpoint: "http://internkim-capability",
HTTPClient: httpClient,
},
ModelName: "gemma",
ExecutionMode: "remote",
}
response, errorValue := client.GenerateRecoveryResponse(context.Background(), "hello")
if errorValue != nil {
t.Fatalf("expected recovery text response: %v", errorValue)
}
if response != "device recovery reply" {
t.Fatalf("expected device recovery response, got %q", response)
}
if strings.Join(receivedExecutionModes, ",") != "auto,device" {
t.Fatalf("expected auto then device execution modes, got %+v", receivedExecutionModes)
}
}
func TestCapabilityLLMClientLocalRecoveryResponseUsesDeviceExecutionMode(t *testing.T) {
var receivedDocument capabilityTextResponseRequestDocument
httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
if errorValue := json.NewDecoder(request.Body).Decode(&receivedDocument); errorValue != nil {
t.Fatalf("expected request document to decode: %v", errorValue)
}
return jsonCapabilityResponse(http.StatusOK, `{"provider":"capabilityLLM","model":"gemma","content":"local failure notice","selectedBackend":"device"}`), nil
}}
client := CapabilityLLMClient{
CapabilityClient: capability.Client{
Endpoint: "http://internkim-capability",
HTTPClient: httpClient,
},
ModelName: "gemma",
ExecutionMode: "remote",
}
response, errorValue := client.GenerateLocalRecoveryResponse(context.Background(), "hello")
if errorValue != nil {
t.Fatalf("expected local recovery response: %v", errorValue)
}
if response != "local failure notice" {
t.Fatalf("expected local recovery response, got %q", response)
}
if receivedDocument.ExecutionMode != "device" {
t.Fatalf("expected device execution mode, got %q", receivedDocument.ExecutionMode)
}
}
func TestCapabilityLLMClientRecoveryChatUsesAutoThenDeviceExecutionModes(t *testing.T) {
receivedExecutionModes := []string{}
requestContext := RequestContext{RequesterPersonID: "person-1", ConversationID: "conversation-1"}
httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
var receivedDocument capabilityChatCompletionRequestDocument
if errorValue := json.NewDecoder(request.Body).Decode(&receivedDocument); errorValue != nil {
t.Fatalf("expected chat request document to decode: %v", errorValue)
}
receivedExecutionModes = append(receivedExecutionModes, receivedDocument.ExecutionMode)
if receivedDocument.Context == nil || *receivedDocument.Context != requestContext {
t.Fatalf("expected requester context %+v, got %+v", requestContext, receivedDocument.Context)
}
if receivedDocument.ExecutionMode == "auto" {
return jsonCapabilityResponse(http.StatusBadGateway, "remote unavailable"), nil
}
return jsonCapabilityResponse(http.StatusOK, `{"finishReason":"stop","provider":"capabilityLLM","model":"gemma","message":{"role":"assistant","content":"device recovery chat"},"selectedBackend":"device"}`), nil
}}
client := CapabilityLLMClient{
CapabilityClient: capability.Client{Endpoint: "http://internkim-capability", HTTPClient: httpClient},
ModelName: "gemma",
ExecutionMode: "remote",
}
responseContext := ContextWithRequestContext(context.Background(), requestContext)
response, errorValue := client.GenerateRecoveryChatCompletion(responseContext, ChatCompletionRequest{
Messages: []ChatCompletionMessage{{Role: "user", Content: "hello"}},
})
if errorValue != nil || response.Message.Content != "device recovery chat" {
t.Fatalf("expected device recovery chat, got %+v, %v", response, errorValue)
}
if strings.Join(receivedExecutionModes, ",") != "auto,device" {
t.Fatalf("expected auto then device execution modes, got %+v", receivedExecutionModes)
}
}
func TestCapabilityLLMClientLocalRecoveryChatUsesDeviceExecutionMode(t *testing.T) {
var receivedDocument capabilityChatCompletionRequestDocument
httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
if errorValue := json.NewDecoder(request.Body).Decode(&receivedDocument); errorValue != nil {
t.Fatalf("expected chat request document to decode: %v", errorValue)
}
return jsonCapabilityResponse(http.StatusOK, `{"finishReason":"stop","provider":"capabilityLLM","model":"gemma","message":{"role":"assistant","content":"local recovery chat"},"selectedBackend":"device"}`), nil
}}
client := CapabilityLLMClient{
CapabilityClient: capability.Client{Endpoint: "http://internkim-capability", HTTPClient: httpClient},
ModelName: "gemma",
ExecutionMode: "remote",
}
response, errorValue := client.GenerateLocalRecoveryChatCompletion(context.Background(), ChatCompletionRequest{})
if errorValue != nil || response.Message.Content != "local recovery chat" {
t.Fatalf("expected local recovery chat, got %+v, %v", response, errorValue)
}
if receivedDocument.ExecutionMode != "device" {
t.Fatalf("expected device execution mode, got %q", receivedDocument.ExecutionMode)
}
}
func TestCapabilityLLMClientLocalRecoveryChatRejectsRemoteBackend(t *testing.T) {
httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
return jsonCapabilityResponse(http.StatusOK, `{"finishReason":"stop","provider":"example-gateway","model":"remote-model","message":{"role":"assistant","content":"remote recovery chat"},"selectedBackend":"remote"}`), nil
}}
client := CapabilityLLMClient{
CapabilityClient: capability.Client{Endpoint: "http://internkim-capability", HTTPClient: httpClient},
ModelName: "gemma",
ExecutionMode: "remote",
}
_, errorValue := client.GenerateLocalRecoveryChatCompletion(context.Background(), ChatCompletionRequest{})
if errorValue == nil || errorValue.Error() != "device recovery chat returned a non-device backend" {
t.Fatalf("expected remote backend rejection, got %v", errorValue)
}
}
func TestRecoveryAttemptContextPreservesDeadlineAndRequester(t *testing.T) {
expectedDeadline := time.Now().Add(time.Minute)
requestContext := RequestContext{RequesterPersonID: "person-1", ConversationID: "conversation-1"}
responseContext := ContextWithRequestContext(context.Background(), requestContext)
responseContext, cancelResponse := context.WithDeadline(responseContext, expectedDeadline)
defer cancelResponse()
recoveryContext, cancelRecovery := recoveryAttemptContext(responseContext)
defer cancelRecovery()
actualDeadline, hasDeadline := recoveryContext.Deadline()
if !hasDeadline || !actualDeadline.Equal(expectedDeadline) {
t.Fatalf("expected recovery deadline %v, got %v", expectedDeadline, actualDeadline)
}
if actualRequestContext := RequestContextFromContext(recoveryContext); actualRequestContext != requestContext {
t.Fatalf("expected requester context %+v, got %+v", requestContext, actualRequestContext)
}
}
type fakeCapabilityHTTPClient struct {
handler func(*http.Request) (*http.Response, error)
}
func (client fakeCapabilityHTTPClient) Do(request *http.Request) (*http.Response, error) {
return client.handler(request)
}
func jsonCapabilityResponse(statusCode int, body string) *http.Response {
return &http.Response{
StatusCode: statusCode,
Body: io.NopCloser(strings.NewReader(body)),
Header: http.Header{"Content-Type": []string{"application/json"}},
}
}
func buildTestStructuredResponseRequest() StructuredResponseRequest {
return StructuredResponseRequest{
Messages: []Message{
{
Role: "user",
Content: "say hello",
},
},
StructuredOutputSchema: StructuredOutputSchema{
Name: "reply",
Document: `{"type":"object","properties":{"reply":{"type":"string"}},"required":["reply"],"additionalProperties":false}`,
IsStrictlyEnforced: true,
},
}
}
func TestCapabilityLLMClientGenerateChatCompletionPrefersRequestModelName(t *testing.T) {
sentModelNames := []string{}
httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
var requestDocument capabilityChatCompletionRequestDocument
if errorValue := json.NewDecoder(request.Body).Decode(&requestDocument); errorValue != nil {
t.Fatalf("expected request document to decode: %v", errorValue)
}
sentModelNames = append(sentModelNames, requestDocument.Model)
return jsonCapabilityResponse(http.StatusOK, `{"finishReason":"stop","provider":"capabilityLLM","model":"answered","message":{"role":"assistant","content":"done"}}`), nil
}}
client := CapabilityLLMClient{
CapabilityClient: capability.Client{
Endpoint: "http://internkim-capability",
HTTPClient: httpClient,
},
ModelName: "configured-model",
}
if _, errorValue := client.GenerateChatCompletion(context.Background(), ChatCompletionRequest{
Messages: []ChatCompletionMessage{{Role: "user", Content: "check"}},
}); errorValue != nil {
t.Fatalf("expected configured model completion: %v", errorValue)
}
if _, errorValue := client.GenerateChatCompletion(context.Background(), ChatCompletionRequest{
ModelName: "requested-model",
Messages: []ChatCompletionMessage{{Role: "user", Content: "check"}},
}); errorValue != nil {
t.Fatalf("expected requested model completion: %v", errorValue)
}
if !reflect.DeepEqual(sentModelNames, []string{"configured-model", "requested-model"}) {
t.Fatalf("expected the request model to win over the client model, got %v", sentModelNames)
}
}
func TestCapabilityLLMClientTellsCapabilitydWhichTierIsAsking(t *testing.T) {
var receivedDocument capabilityChatCompletionRequestDocument
httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
if errorValue := json.NewDecoder(request.Body).Decode(&receivedDocument); errorValue != nil {
t.Fatalf("expected request document to decode: %v", errorValue)
}
return jsonCapabilityResponse(http.StatusOK, `{"finishReason":"stop","provider":"openrouter","upstreamProvider":"Modal","model":"gemma","message":{"role":"assistant","content":"ok"},"usage":{}}`), nil
}}
client := CapabilityLLMClient{
CapabilityClient: capability.Client{Endpoint: "http://internkim-capability", HTTPClient: httpClient},
ModelName: "gemma",
ModelTier: "xlow",
}
response, errorValue := client.GenerateChatCompletion(context.Background(), ChatCompletionRequest{
Messages: []ChatCompletionMessage{{Role: "user", Content: "hello"}},
})
if errorValue != nil {
t.Fatalf("expected chat completion: %v", errorValue)
}
if receivedDocument.ModelTier != "xlow" {
t.Fatalf("capabilityd decides reasoning effort by tier, so the tier must travel, got %q", receivedDocument.ModelTier)
}
if response.UpstreamProvider != "Modal" {
t.Fatalf("the provider that served the turn must reach the ledger, got %q", response.UpstreamProvider)
}
}
func routeBodies(t *testing.T, ctx context.Context) map[string]map[string]any {
t.Helper()
bodiesByPath := map[string]map[string]any{}
httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
body := map[string]any{}
if errorValue := json.NewDecoder(request.Body).Decode(&body); errorValue != nil {
t.Fatalf("expected request document to decode: %v", errorValue)
}
bodiesByPath[request.URL.Path] = body
if request.URL.Path == "/v1/llm/chat" {
return jsonCapabilityResponse(http.StatusOK, `{"provider":"capabilityLLM","model":"gemma","finishReason":"stop","message":{"role":"assistant","content":"done"}}`), nil
}
return jsonCapabilityResponse(http.StatusOK, `{"provider":"capabilityLLM","model":"gemma","content":"{\"reply\":\"ok\"}","selectedBackend":"device_local"}`), nil
}}
client := CapabilityLLMClient{
CapabilityClient: capability.Client{
Endpoint: "http://internkim-capability",
HTTPClient: httpClient,
},
ModelName: "gemma",
}
if _, errorValue := client.GenerateResponse(ctx, "say hello"); errorValue != nil {
t.Fatalf("expected a text response: %v", errorValue)
}
if _, errorValue := client.GenerateStructuredResponse(ctx, buildTestStructuredResponseRequest()); errorValue != nil {
t.Fatalf("expected a structured response: %v", errorValue)
}
chatRequest := ChatCompletionRequest{Messages: []ChatCompletionMessage{{Role: "user", Content: "say hello"}}}
if _, errorValue := client.GenerateChatCompletion(ctx, chatRequest); errorValue != nil {
t.Fatalf("expected a chat completion: %v", errorValue)
}
return bodiesByPath
}
// Capabilityd pins one upstream provider and one prompt cache per session id,
// so every call of one conversation has to name the same one or the turns land
// on providers that cannot see each other's cache.
func TestCapabilityLLMClientSendsTheConversationAsItsSessionID(t *testing.T) {
requestContext := RequestContext{RequesterPersonID: "person-1", ConversationID: "dm:channel-1", Platform: "buzz"}
bodiesByPath := routeBodies(t, ContextWithRequestContext(context.Background(), requestContext))
for _, path := range []string{"/v1/llm/text", "/v1/llm/structured", "/v1/llm/chat"} {
body, isCalled := bodiesByPath[path]
if !isCalled {
t.Fatalf("%s was never asked", path)
}
if body["sessionID"] != "dm:channel-1" {
t.Errorf("%s did not carry the conversation as its session: %v", path, body["sessionID"])
}
}
}
func TestCapabilityLLMClientSendsNoSessionIDWithoutAConversation(t *testing.T) {
bodiesByPath := routeBodies(t, context.Background())
for path, body := range bodiesByPath {
if _, isPresent := body["sessionID"]; isPresent {
t.Errorf("%s invented a session: %v", path, body["sessionID"])
}
}
}
func TestCapabilityLLMClientHandsOnTheWireExchangeCapabilitydSaw(t *testing.T) {
httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
return jsonCapabilityResponse(http.StatusOK, `{"provider":"capabilityLLM","content":"{}","wireExchange":{"endpoint":"https://router.example.com/v1/chat/completions","request":"{\"seed\": 7}","response":"{\"provider\":\"Example\"}"}}`), nil
}}
client := CapabilityLLMClient{CapabilityClient: capability.Client{Endpoint: "http://internkim-capability", HTTPClient: httpClient}}
captureContext, capture := model.WithWireCapture(context.Background())
if _, errorValue := client.GenerateStructuredResponse(captureContext, buildTestStructuredResponseRequest()); errorValue != nil {
t.Fatalf("expected structured response: %v", errorValue)
}
exchange := capture.Exchange()
if exchange == nil || exchange.Endpoint != "https://router.example.com/v1/chat/completions" || exchange.Request != `{"seed": 7}` || exchange.Response != `{"provider":"Example"}` {
t.Fatalf("expected the exchange capabilityd put on the wire, got %+v", exchange)
}
}