all: support openai-compatible models

The support is rather minimal at this point:
Only hard-coded models, only -unsafe, only -skabandaddr="".

The "shared" LLM package is strongly Claude-flavored.

We can fix all of this and more over time, if we are inspired to.
(Maybe we'll switch to https://github.com/maruel/genai?)

The goal for now is to get the rough structure in place.
I've rebased and rebuilt this more times than I care to remember.
diff --git a/loop/agent_test.go b/loop/agent_test.go
index 0924b39..56708e3 100644
--- a/loop/agent_test.go
+++ b/loop/agent_test.go
@@ -11,8 +11,10 @@
 	"testing"
 	"time"
 
-	"sketch.dev/ant"
 	"sketch.dev/httprr"
+	"sketch.dev/llm"
+	"sketch.dev/llm/ant"
+	"sketch.dev/llm/conversation"
 )
 
 // TestAgentLoop tests that the Agent loop functionality works correctly.
@@ -58,7 +60,7 @@
 	if err := os.Chdir("/"); err != nil {
 		t.Fatal(err)
 	}
-	budget := ant.Budget{MaxResponses: 100}
+	budget := conversation.Budget{MaxResponses: 100}
 	wd, err := os.Getwd()
 	if err != nil {
 		t.Fatal(err)
@@ -66,9 +68,11 @@
 
 	apiKey := cmp.Or(os.Getenv("OUTER_SKETCH_ANTHROPIC_API_KEY"), os.Getenv("ANTHROPIC_API_KEY"))
 	cfg := AgentConfig{
-		Context:      ctx,
-		APIKey:       apiKey,
-		HTTPC:        client,
+		Context: ctx,
+		Service: &ant.Service{
+			APIKey: apiKey,
+			HTTPC:  client,
+		},
 		Budget:       budget,
 		GitUsername:  "Test Agent",
 		GitEmail:     "totallyhuman@sketch.dev",
@@ -206,7 +210,7 @@
 func TestAgentProcessTurnWithNilResponse(t *testing.T) {
 	// Create a mock conversation that will return nil and error
 	mockConvo := &MockConvoInterface{
-		sendMessageFunc: func(message ant.Message) (*ant.MessageResponse, error) {
+		sendMessageFunc: func(message llm.Message) (*llm.Response, error) {
 			return nil, fmt.Errorf("test error: simulating nil response")
 		},
 	}
@@ -250,40 +254,40 @@
 
 // MockConvoInterface implements the ConvoInterface for testing
 type MockConvoInterface struct {
-	sendMessageFunc              func(message ant.Message) (*ant.MessageResponse, error)
-	sendUserTextMessageFunc      func(s string, otherContents ...ant.Content) (*ant.MessageResponse, error)
-	toolResultContentsFunc       func(ctx context.Context, resp *ant.MessageResponse) ([]ant.Content, error)
-	toolResultCancelContentsFunc func(resp *ant.MessageResponse) ([]ant.Content, error)
+	sendMessageFunc              func(message llm.Message) (*llm.Response, error)
+	sendUserTextMessageFunc      func(s string, otherContents ...llm.Content) (*llm.Response, error)
+	toolResultContentsFunc       func(ctx context.Context, resp *llm.Response) ([]llm.Content, error)
+	toolResultCancelContentsFunc func(resp *llm.Response) ([]llm.Content, error)
 	cancelToolUseFunc            func(toolUseID string, cause error) error
-	cumulativeUsageFunc          func() ant.CumulativeUsage
-	resetBudgetFunc              func(ant.Budget)
+	cumulativeUsageFunc          func() conversation.CumulativeUsage
+	resetBudgetFunc              func(conversation.Budget)
 	overBudgetFunc               func() error
 	getIDFunc                    func() string
-	subConvoWithHistoryFunc      func() *ant.Convo
+	subConvoWithHistoryFunc      func() *conversation.Convo
 }
 
-func (m *MockConvoInterface) SendMessage(message ant.Message) (*ant.MessageResponse, error) {
+func (m *MockConvoInterface) SendMessage(message llm.Message) (*llm.Response, error) {
 	if m.sendMessageFunc != nil {
 		return m.sendMessageFunc(message)
 	}
 	return nil, nil
 }
 
-func (m *MockConvoInterface) SendUserTextMessage(s string, otherContents ...ant.Content) (*ant.MessageResponse, error) {
+func (m *MockConvoInterface) SendUserTextMessage(s string, otherContents ...llm.Content) (*llm.Response, error) {
 	if m.sendUserTextMessageFunc != nil {
 		return m.sendUserTextMessageFunc(s, otherContents...)
 	}
 	return nil, nil
 }
 
-func (m *MockConvoInterface) ToolResultContents(ctx context.Context, resp *ant.MessageResponse) ([]ant.Content, error) {
+func (m *MockConvoInterface) ToolResultContents(ctx context.Context, resp *llm.Response) ([]llm.Content, error) {
 	if m.toolResultContentsFunc != nil {
 		return m.toolResultContentsFunc(ctx, resp)
 	}
 	return nil, nil
 }
 
-func (m *MockConvoInterface) ToolResultCancelContents(resp *ant.MessageResponse) ([]ant.Content, error) {
+func (m *MockConvoInterface) ToolResultCancelContents(resp *llm.Response) ([]llm.Content, error) {
 	if m.toolResultCancelContentsFunc != nil {
 		return m.toolResultCancelContentsFunc(resp)
 	}
@@ -297,14 +301,14 @@
 	return nil
 }
 
-func (m *MockConvoInterface) CumulativeUsage() ant.CumulativeUsage {
+func (m *MockConvoInterface) CumulativeUsage() conversation.CumulativeUsage {
 	if m.cumulativeUsageFunc != nil {
 		return m.cumulativeUsageFunc()
 	}
-	return ant.CumulativeUsage{}
+	return conversation.CumulativeUsage{}
 }
 
-func (m *MockConvoInterface) ResetBudget(budget ant.Budget) {
+func (m *MockConvoInterface) ResetBudget(budget conversation.Budget) {
 	if m.resetBudgetFunc != nil {
 		m.resetBudgetFunc(budget)
 	}
@@ -324,7 +328,7 @@
 	return "mock-convo-id"
 }
 
-func (m *MockConvoInterface) SubConvoWithHistory() *ant.Convo {
+func (m *MockConvoInterface) SubConvoWithHistory() *conversation.Convo {
 	if m.subConvoWithHistoryFunc != nil {
 		return m.subConvoWithHistoryFunc()
 	}
@@ -337,7 +341,7 @@
 func TestAgentProcessTurnWithNilResponseNilError(t *testing.T) {
 	// Create a mock conversation that will return nil response and nil error
 	mockConvo := &MockConvoInterface{
-		sendMessageFunc: func(message ant.Message) (*ant.MessageResponse, error) {
+		sendMessageFunc: func(message llm.Message) (*llm.Response, error) {
 			return nil, nil // This is unusual but now handled gracefully
 		},
 	}
@@ -464,48 +468,48 @@
 
 // mockConvoInterface is a mock implementation of ConvoInterface for testing
 type mockConvoInterface struct {
-	SendMessageFunc        func(message ant.Message) (*ant.MessageResponse, error)
-	ToolResultContentsFunc func(ctx context.Context, resp *ant.MessageResponse) ([]ant.Content, error)
+	SendMessageFunc        func(message llm.Message) (*llm.Response, error)
+	ToolResultContentsFunc func(ctx context.Context, resp *llm.Response) ([]llm.Content, error)
 }
 
 func (c *mockConvoInterface) GetID() string {
 	return "mockConvoInterface-id"
 }
 
-func (c *mockConvoInterface) SubConvoWithHistory() *ant.Convo {
+func (c *mockConvoInterface) SubConvoWithHistory() *conversation.Convo {
 	return nil
 }
 
-func (m *mockConvoInterface) CumulativeUsage() ant.CumulativeUsage {
-	return ant.CumulativeUsage{}
+func (m *mockConvoInterface) CumulativeUsage() conversation.CumulativeUsage {
+	return conversation.CumulativeUsage{}
 }
 
-func (m *mockConvoInterface) ResetBudget(ant.Budget) {}
+func (m *mockConvoInterface) ResetBudget(conversation.Budget) {}
 
 func (m *mockConvoInterface) OverBudget() error {
 	return nil
 }
 
-func (m *mockConvoInterface) SendMessage(message ant.Message) (*ant.MessageResponse, error) {
+func (m *mockConvoInterface) SendMessage(message llm.Message) (*llm.Response, error) {
 	if m.SendMessageFunc != nil {
 		return m.SendMessageFunc(message)
 	}
-	return &ant.MessageResponse{StopReason: ant.StopReasonEndTurn}, nil
+	return &llm.Response{StopReason: llm.StopReasonEndTurn}, nil
 }
 
-func (m *mockConvoInterface) SendUserTextMessage(s string, otherContents ...ant.Content) (*ant.MessageResponse, error) {
-	return m.SendMessage(ant.UserStringMessage(s))
+func (m *mockConvoInterface) SendUserTextMessage(s string, otherContents ...llm.Content) (*llm.Response, error) {
+	return m.SendMessage(llm.UserStringMessage(s))
 }
 
-func (m *mockConvoInterface) ToolResultContents(ctx context.Context, resp *ant.MessageResponse) ([]ant.Content, error) {
+func (m *mockConvoInterface) ToolResultContents(ctx context.Context, resp *llm.Response) ([]llm.Content, error) {
 	if m.ToolResultContentsFunc != nil {
 		return m.ToolResultContentsFunc(ctx, resp)
 	}
-	return []ant.Content{}, nil
+	return []llm.Content{}, nil
 }
 
-func (m *mockConvoInterface) ToolResultCancelContents(resp *ant.MessageResponse) ([]ant.Content, error) {
-	return []ant.Content{ant.StringContent("Tool use cancelled")}, nil
+func (m *mockConvoInterface) ToolResultCancelContents(resp *llm.Response) ([]llm.Content, error) {
+	return []llm.Content{llm.StringContent("Tool use cancelled")}, nil
 }
 
 func (m *mockConvoInterface) CancelToolUse(toolUseID string, cause error) error {
@@ -542,11 +546,11 @@
 	agent.inbox <- "Test message"
 
 	// Setup the mock to simulate a model response with end of turn
-	mockConvo.SendMessageFunc = func(message ant.Message) (*ant.MessageResponse, error) {
-		return &ant.MessageResponse{
-			StopReason: ant.StopReasonEndTurn,
-			Content: []ant.Content{
-				ant.StringContent("This is a test response"),
+	mockConvo.SendMessageFunc = func(message llm.Message) (*llm.Response, error) {
+		return &llm.Response{
+			StopReason: llm.StopReasonEndTurn,
+			Content: []llm.Content{
+				llm.StringContent("This is a test response"),
 			},
 		}, nil
 	}
@@ -615,29 +619,29 @@
 
 	// First response requests a tool
 	firstResponseDone := false
-	mockConvo.SendMessageFunc = func(message ant.Message) (*ant.MessageResponse, error) {
+	mockConvo.SendMessageFunc = func(message llm.Message) (*llm.Response, error) {
 		if !firstResponseDone {
 			firstResponseDone = true
-			return &ant.MessageResponse{
-				StopReason: ant.StopReasonToolUse,
-				Content: []ant.Content{
-					ant.StringContent("I'll use a tool"),
-					{Type: ant.ContentTypeToolUse, ToolName: "test_tool", ToolInput: []byte("{}"), ID: "test_id"},
+			return &llm.Response{
+				StopReason: llm.StopReasonToolUse,
+				Content: []llm.Content{
+					llm.StringContent("I'll use a tool"),
+					{Type: llm.ContentTypeToolUse, ToolName: "test_tool", ToolInput: []byte("{}"), ID: "test_id"},
 				},
 			}, nil
 		}
 		// Second response ends the turn
-		return &ant.MessageResponse{
-			StopReason: ant.StopReasonEndTurn,
-			Content: []ant.Content{
-				ant.StringContent("Finished using the tool"),
+		return &llm.Response{
+			StopReason: llm.StopReasonEndTurn,
+			Content: []llm.Content{
+				llm.StringContent("Finished using the tool"),
 			},
 		}, nil
 	}
 
 	// Tool result content handler
-	mockConvo.ToolResultContentsFunc = func(ctx context.Context, resp *ant.MessageResponse) ([]ant.Content, error) {
-		return []ant.Content{ant.StringContent("Tool executed successfully")}, nil
+	mockConvo.ToolResultContentsFunc = func(ctx context.Context, resp *llm.Response) ([]llm.Content, error) {
+		return []llm.Content{llm.StringContent("Tool executed successfully")}, nil
 	}
 
 	// Track state transitions