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/dockerimg/createdockerfile.go b/dockerimg/createdockerfile.go
index 12e876a..75ece83 100644
--- a/dockerimg/createdockerfile.go
+++ b/dockerimg/createdockerfile.go
@@ -15,7 +15,8 @@
 	"strings"
 	"text/template"
 
-	"sketch.dev/ant"
+	"sketch.dev/llm"
+	"sketch.dev/llm/conversation"
 )
 
 func hashInitFiles(initFiles map[string]string) string {
@@ -166,7 +167,7 @@
 // It expects the relevant initFiles to have been provided.
 // If the sketch binary is being executed in a sub-directory of the repository,
 // the relative path is provided on subPathWorkingDir.
-func createDockerfile(ctx context.Context, httpc *http.Client, antURL, antAPIKey string, initFiles map[string]string, subPathWorkingDir string) (string, error) {
+func createDockerfile(ctx context.Context, srv llm.Service, initFiles map[string]string, subPathWorkingDir string) (string, error) {
 	if subPathWorkingDir == "." {
 		subPathWorkingDir = ""
 	} else if subPathWorkingDir != "" && subPathWorkingDir[0] != '/' {
@@ -188,18 +189,14 @@
 		toolCalled = true
 		return "OK", nil
 	}
-	convo := ant.NewConvo(ctx, antAPIKey)
-	if httpc != nil {
-		convo.HTTPC = httpc
-	}
-	if antURL != "" {
-		convo.URL = antURL
-	}
-	convo.Tools = []*ant.Tool{{
+
+	convo := conversation.New(ctx, srv)
+
+	convo.Tools = []*llm.Tool{{
 		Name:        "dockerfile",
 		Description: "Helps define a Dockerfile that sets up a dev environment for this project.",
 		Run:         runDockerfile,
-		InputSchema: ant.MustSchema(`{
+		InputSchema: llm.MustSchema(`{
   "type": "object",
   "required": ["extra_cmds"],
   "properties": {
@@ -223,10 +220,10 @@
 	//	git diff dockerimg/testdata/*.dockerfile
 	//
 	// If the dockerfile changes are a strict improvement, commit all the changes.
-	msg := ant.Message{
-		Role: ant.MessageRoleUser,
-		Content: []ant.Content{{
-			Type: ant.ContentTypeText,
+	msg := llm.Message{
+		Role: llm.MessageRoleUser,
+		Content: []llm.Content{{
+			Type: llm.ContentTypeText,
 			Text: `
 Call the dockerfile tool to create a Dockerfile.
 The parameters to dockerfile fill out the From and ExtraCmds
@@ -250,15 +247,15 @@
 	}
 
 	for _, name := range slices.Sorted(maps.Keys(initFiles)) {
-		msg.Content = append(msg.Content, ant.StringContent(fmt.Sprintf("Here is the contents %s:\n<file>\n%s\n</file>\n\n", name, initFiles[name])))
+		msg.Content = append(msg.Content, llm.StringContent(fmt.Sprintf("Here is the contents %s:\n<file>\n%s\n</file>\n\n", name, initFiles[name])))
 	}
-	msg.Content = append(msg.Content, ant.StringContent("Now call the dockerfile tool."))
+	msg.Content = append(msg.Content, llm.StringContent("Now call the dockerfile tool."))
 	res, err := convo.SendMessage(msg)
 	if err != nil {
 		return "", err
 	}
-	if res.StopReason != ant.StopReasonToolUse {
-		return "", fmt.Errorf("expected stop reason %q, got %q", ant.StopReasonToolUse, res.StopReason)
+	if res.StopReason != llm.StopReasonToolUse {
+		return "", fmt.Errorf("expected stop reason %q, got %q", llm.StopReasonToolUse, res.StopReason)
 	}
 	if _, err := convo.ToolResultContents(context.TODO(), res); err != nil {
 		return "", err
diff --git a/dockerimg/dockerimg.go b/dockerimg/dockerimg.go
index 292c8b6..1486435 100644
--- a/dockerimg/dockerimg.go
+++ b/dockerimg/dockerimg.go
@@ -21,6 +21,7 @@
 	"time"
 
 	"sketch.dev/browser"
+	"sketch.dev/llm/ant"
 	"sketch.dev/loop/server"
 	"sketch.dev/skribe"
 	"sketch.dev/webui"
@@ -654,7 +655,12 @@
 		}
 
 		start := time.Now()
-		dockerfile, err := createDockerfile(ctx, http.DefaultClient, antURL, antAPIKey, initFiles, subPathWorkingDir)
+		srv := &ant.Service{
+			URL:    antURL,
+			APIKey: antAPIKey,
+			HTTPC:  http.DefaultClient,
+		}
+		dockerfile, err := createDockerfile(ctx, srv, initFiles, subPathWorkingDir)
 		if err != nil {
 			return "", fmt.Errorf("create dockerfile: %w", err)
 		}
diff --git a/dockerimg/dockerimg_test.go b/dockerimg/dockerimg_test.go
index 9e39e9c..7e41742 100644
--- a/dockerimg/dockerimg_test.go
+++ b/dockerimg/dockerimg_test.go
@@ -13,6 +13,7 @@
 
 	gcmp "github.com/google/go-cmp/cmp"
 	"sketch.dev/httprr"
+	"sketch.dev/llm/ant"
 )
 
 var flagRewriteWant = flag.Bool("rewritewant", false, "rewrite the dockerfiles we want from the model")
@@ -89,7 +90,11 @@
 				t.Fatal(err)
 			}
 			apiKey := cmp.Or(os.Getenv("OUTER_SKETCH_ANTHROPIC_API_KEY"), os.Getenv("ANTHROPIC_API_KEY"))
-			result, err := createDockerfile(ctx, rr.Client(), "", apiKey, initFiles, "")
+			srv := &ant.Service{
+				APIKey: apiKey,
+				HTTPC:  rr.Client(),
+			}
+			result, err := createDockerfile(ctx, srv, initFiles, "")
 			if err != nil {
 				t.Fatal(err)
 			}