on main help sign in
Update readme & use provided logging package
12a98d6Pietjan Oostra
README.md+30
@@ -100,6 +100,7 @@ You can configure OpenCode using environment variables:
100| `AZURE_OPENAI_ENDPOINT` | For Azure OpenAI models |
100| `AZURE_OPENAI_ENDPOINT` | For Azure OpenAI models |
101| `AZURE_OPENAI_API_KEY` | For Azure OpenAI models (optional when using Entra ID) |
101| `AZURE_OPENAI_API_KEY` | For Azure OpenAI models (optional when using Entra ID) |
102| `AZURE_OPENAI_API_VERSION` | For Azure OpenAI models |
102| `AZURE_OPENAI_API_VERSION` | For Azure OpenAI models |
103+| `LOCAL_ENDPOINT` | For self-hosted models |
103| `SHELL` | Default shell to use (if not specified in config) |
104| `SHELL` | Default shell to use (if not specified in config) |
104
105
105### Shell Configuration
106### Shell Configuration
@@ -566,6 +567,35 @@ The AI assistant can access LSP features through the `diagnostics` tool, allowin
566
567
567While the LSP client implementation supports the full LSP protocol (including completions, hover, definition, etc.), currently only diagnostics are exposed to the AI assistant.
568While the LSP client implementation supports the full LSP protocol (including completions, hover, definition, etc.), currently only diagnostics are exposed to the AI assistant.
568
569
570+## Using a self-hosted model provider
571+
572+OpenCode can also load and use models from a self-hosted (OpenAI-like) provider.
573+This is useful for developers who want to experiment with custom models.
574+
575+### Configuring a self-hosted provider
576+
577+You can use a self-hosted model by setting the `LOCAL_ENDPOINT` environment variable.
578+This will cause OpenCode to load and use the models from the specified endpoint.
579+
580+```bash
581+LOCAL_ENDPOINT=http://localhost:1235/v1
582+```
583+
584+### Configuring a self-hosted model
585+
586+You can also configure a self-hosted model in the configuration file under the `agents` section:
587+
588+```json
589+{
590+ "agents": {
591+ "coder": {
592+ "model": "local.granite-3.3-2b-instruct@q8_0",
593+ "reasoningEffort": "high"
594+ }
595+ }
596+}
597+```
598+
569## Development
599## Development
570
600
571### Prerequisites
601### Prerequisites
internal/llm/models/local.go+24−9
@@ -3,7 +3,6 @@ package models
3import (
3import (
4 "cmp"
4 "cmp"
5 "encoding/json"
5 "encoding/json"
6− "log/slog"
7 "net/http"
6 "net/http"
8 "net/url"
7 "net/url"
9 "os"
8 "os"
@@ -11,6 +10,7 @@ import (
11 "strings"
10 "strings"
12 "unicode"
11 "unicode"
13
12
13+ "github.com/opencode-ai/opencode/internal/logging"
14 "github.com/spf13/viper"
14 "github.com/spf13/viper"
15)
15)
16
16
@@ -25,7 +25,7 @@ func init() {
25 if endpoint := os.Getenv("LOCAL_ENDPOINT"); endpoint != "" {
25 if endpoint := os.Getenv("LOCAL_ENDPOINT"); endpoint != "" {
26 localEndpoint, err := url.Parse(endpoint)
26 localEndpoint, err := url.Parse(endpoint)
27 if err != nil {
27 if err != nil {
28− slog.Debug("Failed to parse local endpoint",
28+ logging.Debug("Failed to parse local endpoint",
29 "error", err,
29 "error", err,
30 "endpoint", endpoint,
30 "endpoint", endpoint,
31 )
31 )
@@ -44,7 +44,7 @@ func init() {
44 }
44 }
45
45
46 if len(models) == 0 {
46 if len(models) == 0 {
47− slog.Debug("No local models found",
47+ logging.Debug("No local models found",
48 "endpoint", endpoint,
48 "endpoint", endpoint,
49 )
49 )
50 return
50 return
@@ -77,7 +77,7 @@ type localModel struct {
77func listLocalModels(modelsEndpoint string) []localModel {
77func listLocalModels(modelsEndpoint string) []localModel {
78 res, err := http.Get(modelsEndpoint)
78 res, err := http.Get(modelsEndpoint)
79 if err != nil {
79 if err != nil {
80− slog.Debug("Failed to list local models",
80+ logging.Debug("Failed to list local models",
81 "error", err,
81 "error", err,
82 "endpoint", modelsEndpoint,
82 "endpoint", modelsEndpoint,
83 )
83 )
@@ -85,7 +85,7 @@ func listLocalModels(modelsEndpoint string) []localModel {
85 defer res.Body.Close()
85 defer res.Body.Close()
86
86
87 if res.StatusCode != http.StatusOK {
87 if res.StatusCode != http.StatusOK {
88− slog.Debug("Failed to list local models",
88+ logging.Debug("Failed to list local models",
89 "status", res.StatusCode,
89 "status", res.StatusCode,
90 "endpoint", modelsEndpoint,
90 "endpoint", modelsEndpoint,
91 )
91 )
@@ -93,7 +93,7 @@ func listLocalModels(modelsEndpoint string) []localModel {
93
93
94 var modelList localModelList
94 var modelList localModelList
95 if err = json.NewDecoder(res.Body).Decode(&modelList); err != nil {
95 if err = json.NewDecoder(res.Body).Decode(&modelList); err != nil {
96− slog.Debug("Failed to list local models",
96+ logging.Debug("Failed to list local models",
97 "error", err,
97 "error", err,
98 "endpoint", modelsEndpoint,
98 "endpoint", modelsEndpoint,
99 )
99 )
@@ -103,7 +103,7 @@ func listLocalModels(modelsEndpoint string) []localModel {
103 for _, model := range modelList.Data {
103 for _, model := range modelList.Data {
104 if strings.HasSuffix(modelsEndpoint, lmStudioBetaModelsPath) {
104 if strings.HasSuffix(modelsEndpoint, lmStudioBetaModelsPath) {
105 if model.Object != "model" || model.Type != "llm" {
105 if model.Object != "model" || model.Type != "llm" {
106− slog.Debug("Skipping unsupported LMStudio model",
106+ logging.Debug("Skipping unsupported LMStudio model",
107 "endpoint", modelsEndpoint,
107 "endpoint", modelsEndpoint,
108 "id", model.ID,
108 "id", model.ID,
109 "object", model.Object,
109 "object", model.Object,
@@ -125,7 +125,7 @@ func loadLocalModels(models []localModel) {
125 model := convertLocalModel(m)
125 model := convertLocalModel(m)
126 SupportedModels[model.ID] = model
126 SupportedModels[model.ID] = model
127
127
128− if i == 1 || m.State == "loaded" {
128+ if i == 0 || m.State == "loaded" {
129 viper.SetDefault("agents.coder.model", model.ID)
129 viper.SetDefault("agents.coder.model", model.ID)
130 viper.SetDefault("agents.summarizer.model", model.ID)
130 viper.SetDefault("agents.summarizer.model", model.ID)
131 viper.SetDefault("agents.task.model", model.ID)
131 viper.SetDefault("agents.task.model", model.ID)
@@ -150,7 +150,19 @@ func convertLocalModel(model localModel) Model {
150var modelInfoRegex = regexp.MustCompile(`(?i)^([a-z0-9]+)(?:[-_]?([rv]?\d[\.\d]*))?(?:[-_]?([a-z]+))?.*`)
150var modelInfoRegex = regexp.MustCompile(`(?i)^([a-z0-9]+)(?:[-_]?([rv]?\d[\.\d]*))?(?:[-_]?([a-z]+))?.*`)
151
151
152func friendlyModelName(modelID string) string {
152func friendlyModelName(modelID string) string {
153+ mainID := modelID
154+ tag := ""
155+
156+ if slash := strings.LastIndex(mainID, "/"); slash != -1 {
157+ mainID = mainID[slash+1:]
158+ }
159+
160+ if at := strings.Index(modelID, "@"); at != -1 {
161+ mainID = modelID[:at]
162+ tag = modelID[at+1:]
163+ }
164+
153− match := modelInfoRegex.FindStringSubmatch(modelID)
165+ match := modelInfoRegex.FindStringSubmatch(mainID)
154 if match == nil {
166 if match == nil {
155 return modelID
167 return modelID
156 }
168 }
@@ -186,6 +198,9 @@ func friendlyModelName(modelID string) string {
186 if label != "" {
198 if label != "" {
187 parts = append(parts, label)
199 parts = append(parts, label)
188 }
200 }
201+ if tag != "" {
202+ parts = append(parts, tag)
203+ }
189
204
190 return strings.Join(parts, " ")
205 return strings.Join(parts, " ")
191}
206}