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/mocks.go b/loop/mocks.go
index 7e05070..811ab2c 100644
--- a/loop/mocks.go
+++ b/loop/mocks.go
@@ -6,10 +6,11 @@
 	"sync"
 	"testing"
 
-	"sketch.dev/ant"
+	"sketch.dev/llm"
+	"sketch.dev/llm/conversation"
 )
 
-// MockConvo is a custom mock for ant.Convo interface
+// MockConvo is a custom mock for conversation.Convo interface
 type MockConvo struct {
 	mu sync.Mutex
 	t  *testing.T
@@ -21,23 +22,23 @@
 }
 
 type mockCall struct {
-	args   []interface{}
-	result []interface{}
+	args   []any
+	result []any
 }
 
 type mockExpectation struct {
 	until  chan any
-	args   []interface{}
-	result []interface{}
+	args   []any
+	result []any
 }
 
 // Return sets up return values for an expectation
-func (e *mockExpectation) Return(values ...interface{}) {
+func (e *mockExpectation) Return(values ...any) {
 	e.result = values
 }
 
 // Return sets up return values for an expectation
-func (e *mockExpectation) BlockAndReturn(until chan any, values ...interface{}) {
+func (e *mockExpectation) BlockAndReturn(until chan any, values ...any) {
 	e.until = until
 	e.result = values
 }
@@ -53,7 +54,7 @@
 }
 
 // ExpectCall sets up an expectation for a method call
-func (m *MockConvo) ExpectCall(method string, args ...interface{}) *mockExpectation {
+func (m *MockConvo) ExpectCall(method string, args ...any) *mockExpectation {
 	m.mu.Lock()
 	defer m.mu.Unlock()
 	expectation := &mockExpectation{args: args}
@@ -65,7 +66,7 @@
 }
 
 // findMatchingExpectation finds a matching expectation for a method call
-func (m *MockConvo) findMatchingExpectation(method string, args ...interface{}) (*mockExpectation, bool) {
+func (m *MockConvo) findMatchingExpectation(method string, args ...any) (*mockExpectation, bool) {
 	m.mu.Lock()
 	defer m.mu.Unlock()
 	expectations, ok := m.expectations[method]
@@ -87,7 +88,7 @@
 }
 
 // matchArgs checks if call arguments match expectation arguments
-func matchArgs(expected, actual []interface{}) bool {
+func matchArgs(expected, actual []any) bool {
 	if len(expected) != len(actual) {
 		return false
 	}
@@ -107,7 +108,7 @@
 }
 
 // recordCall records a method call
-func (m *MockConvo) recordCall(method string, args ...interface{}) {
+func (m *MockConvo) recordCall(method string, args ...any) {
 	m.mu.Lock()
 	defer m.mu.Unlock()
 	if _, ok := m.calls[method]; !ok {
@@ -116,7 +117,7 @@
 	m.calls[method] = append(m.calls[method], &mockCall{args: args})
 }
 
-func (m *MockConvo) SendMessage(message ant.Message) (*ant.MessageResponse, error) {
+func (m *MockConvo) SendMessage(message llm.Message) (*llm.Response, error) {
 	m.recordCall("SendMessage", message)
 	exp, ok := m.findMatchingExpectation("SendMessage", message)
 	if !ok {
@@ -129,10 +130,10 @@
 	if err, ok := exp.result[1].(error); ok {
 		retErr = err
 	}
-	return exp.result[0].(*ant.MessageResponse), retErr
+	return exp.result[0].(*llm.Response), retErr
 }
 
-func (m *MockConvo) SendUserTextMessage(message string, otherContents ...ant.Content) (*ant.MessageResponse, error) {
+func (m *MockConvo) SendUserTextMessage(message string, otherContents ...llm.Content) (*llm.Response, error) {
 	m.recordCall("SendUserTextMessage", message, otherContents)
 	exp, ok := m.findMatchingExpectation("SendUserTextMessage", message, otherContents)
 	if !ok {
@@ -145,10 +146,10 @@
 	if err, ok := exp.result[1].(error); ok {
 		retErr = err
 	}
-	return exp.result[0].(*ant.MessageResponse), retErr
+	return exp.result[0].(*llm.Response), retErr
 }
 
-func (m *MockConvo) ToolResultContents(ctx context.Context, resp *ant.MessageResponse) ([]ant.Content, error) {
+func (m *MockConvo) ToolResultContents(ctx context.Context, resp *llm.Response) ([]llm.Content, error) {
 	m.recordCall("ToolResultContents", resp)
 	exp, ok := m.findMatchingExpectation("ToolResultContents", resp)
 	if !ok {
@@ -162,10 +163,10 @@
 		retErr = err
 	}
 
-	return exp.result[0].([]ant.Content), retErr
+	return exp.result[0].([]llm.Content), retErr
 }
 
-func (m *MockConvo) ToolResultCancelContents(resp *ant.MessageResponse) ([]ant.Content, error) {
+func (m *MockConvo) ToolResultCancelContents(resp *llm.Response) ([]llm.Content, error) {
 	m.recordCall("ToolResultCancelContents", resp)
 	exp, ok := m.findMatchingExpectation("ToolResultCancelContents", resp)
 	if !ok {
@@ -179,12 +180,12 @@
 		retErr = err
 	}
 
-	return exp.result[0].([]ant.Content), retErr
+	return exp.result[0].([]llm.Content), retErr
 }
 
-func (m *MockConvo) CumulativeUsage() ant.CumulativeUsage {
+func (m *MockConvo) CumulativeUsage() conversation.CumulativeUsage {
 	m.recordCall("CumulativeUsage")
-	return ant.CumulativeUsage{}
+	return conversation.CumulativeUsage{}
 }
 
 func (m *MockConvo) OverBudget() error {
@@ -197,12 +198,12 @@
 	return "mock-conversation-id"
 }
 
-func (m *MockConvo) SubConvoWithHistory() *ant.Convo {
+func (m *MockConvo) SubConvoWithHistory() *conversation.Convo {
 	m.recordCall("SubConvoWithHistory")
 	return nil
 }
 
-func (m *MockConvo) ResetBudget(_ ant.Budget) {
+func (m *MockConvo) ResetBudget(_ conversation.Budget) {
 	m.recordCall("ResetBudget")
 }