feat: replace provider layer with Google Genkit Go SDK#5
Conversation
Replace all hand-rolled provider implementations with Google Genkit Go SDK adapters. Keeps provider.Provider interface unchanged — Genkit is an internal implementation detail. All consumers (executor, ratchet-cli, mesh) unaffected. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
8-task plan covering: dependency setup, type conversion, adapter, factory functions, registry update, file deletion, dep cleanup, tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add github.com/firebase/genkit/go@v1.6.0 and its plugins (anthropic, googlegenai, ollama, compat_oai) as dependencies. Create genkit/ package with a lazily-initialized shared Genkit instance for test/mock scenarios. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement bidirectional conversion between provider.Message/Response/
StreamEvent types and Genkit ai.Message/ModelResponse/ModelResponseChunk.
Includes thinking trace support mapping ai.PartReasoning to StreamEvent
{Type:"thinking"} per the critical alignment requirement.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add genkitProvider struct implementing provider.Provider via Genkit's Generate and GenerateStream APIs. Tools are registered lazily per unique name to prevent duplicate registration panics. Streaming extracts thinking/tool calls from the final response. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement factory functions for Anthropic, OpenAI, Google AI, Ollama, OpenAI-compatible (OpenRouter/Copilot/Cohere/llama.cpp), Azure OpenAI, Anthropic Foundry, Vertex AI, and AWS Bedrock. Each factory creates a Genkit instance with the appropriate plugin. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace all provider.New*Provider() calls in ProviderRegistry factory functions with gkprov.New*Provider() calls that create Genkit-backed providers. OpenAI-compatible factories (openrouter, copilot, cohere, copilot_models, llama_cpp) route through NewOpenAICompatibleProvider. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Delete 26 hand-rolled provider files and replace their usage in provider_registry.go, module_provider.go, and step_model_pull.go with Genkit factory calls. Preserve anthropic_bedrock.go (still used by genkit/providers.go) alongside new anthropic_bedrock_convert.go. Add OllamaClient for Pull/ListModels utility access in step_model_pull. Move models.go constants/types/helpers from deleted files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Demotes golang.org/x/oauth2 to indirect — no longer directly used after removing old hand-rolled provider implementations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Integration tests behind //go:build integration tag. Each test skips if the required API key env var is unset, covering: - Anthropic, OpenAI, Google AI, Ollama, OpenRouter - AWS Bedrock, Vertex AI, Azure OpenAI, Anthropic Foundry - provider.Provider interface satisfaction - streaming thinking trace propagation Run with: go test -tags integration ./genkit/... Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Store maxTokens in genkitProvider struct and apply it to every
Chat/Stream call via ai.WithConfig(&ai.GenerationCommonConfig{
MaxOutputTokens: maxTokens}) when non-zero. All provider factory
functions now populate the field.
Fixes spec-reviewer Issue 2: maxTokens was silently dropped.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rovider cache - genkit/providers.go: when credentialsJSON is non-empty, write it to a temp file and set GOOGLE_APPLICATION_CREDENTIALS before calling gk.Init so Genkit's VertexAI plugin picks it up via credentials.DetectDefault(). Previously the parameter was accepted but silently ignored. - orchestrator/provider_registry.go: re-check the cache under the write lock in createAndCache before inserting, eliminating the TOCTOU race where two concurrent callers for the same uncached alias both created a provider. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR migrates the agent’s provider implementation layer from multiple hand-rolled SDK/HTTP integrations to a unified set of Genkit Go SDK adapters, while keeping the existing provider.Provider interface stable for the rest of the codebase.
Changes:
- Introduces a new
genkit/package implementingprovider.Providervia Genkit plugins (plus unit + integration tests). - Updates provider registries and module factories to construct Genkit-backed providers and fixes a ProviderRegistry cache TOCTOU race.
- Removes legacy provider implementations/tests and replaces Ollama “utility” operations (list/pull/health) with a dedicated
OllamaClient.
Reviewed changes
Copilot reviewed 43 out of 44 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| step_model_pull.go | Switches Ollama model pull/list to provider.OllamaClient. |
| provider/openrouter.go | Removes legacy OpenRouter provider implementation. |
| provider/openrouter_test.go | Removes legacy OpenRouter provider tests. |
| provider/openai.go | Removes legacy OpenAI provider implementation. |
| provider/openai_convert.go | Removes legacy OpenAI conversion/helpers. |
| provider/openai_azure.go | Removes legacy Azure OpenAI provider implementation. |
| provider/openai_azure_test.go | Removes legacy Azure OpenAI provider tests. |
| provider/ollama.go | Removes legacy Ollama provider implementation. |
| provider/ollama_test.go | Removes legacy Ollama provider tests. |
| provider/ollama_convert.go | Removes legacy Ollama conversion/helpers. |
| provider/ollama_convert_test.go | Removes legacy Ollama conversion tests. |
| provider/ollama_client.go | Adds Ollama utility client (pull/list/health). |
| provider/models.go | Moves/shared model-listing constants and updates Ollama model listing to use OllamaClient; adds Gemini model listing via genai SDK. |
| provider/llama_cpp.go | Removes legacy llama.cpp provider implementation. |
| provider/llama_cpp_test.go | Removes legacy llama.cpp provider tests. |
| provider/gemini.go | Removes legacy Gemini provider implementation. |
| provider/gemini_test.go | Removes legacy Gemini provider tests. |
| provider/copilot.go | Removes legacy Copilot provider implementation. |
| provider/copilot_test.go | Removes legacy Copilot provider tests. |
| provider/copilot_models.go | Removes legacy GitHub Models provider implementation. |
| provider/copilot_models_test.go | Removes legacy GitHub Models provider tests. |
| provider/cohere.go | Removes legacy Cohere provider implementation. |
| provider/anthropic.go | Removes legacy Anthropic provider implementation (direct API). |
| provider/anthropic_vertex.go | Removes legacy Anthropic Vertex provider implementation. |
| provider/anthropic_vertex_test.go | Removes legacy Anthropic Vertex provider tests. |
| provider/anthropic_foundry.go | Removes legacy Anthropic Foundry provider implementation. |
| provider/anthropic_foundry_test.go | Removes legacy Anthropic Foundry provider tests. |
| provider/anthropic_convert_test.go | Removes legacy Anthropic conversion tests. |
| provider/anthropic_bedrock_convert.go | Restores defaultAnthropicMaxTokens constant needed by remaining Bedrock codepaths. |
| provider_registry.go | Updates agent-level ProviderRegistry factories to use Genkit-backed constructors. |
| orchestrator/provider_registry.go | Updates orchestrator ProviderRegistry factories to use Genkit-backed constructors; fixes TOCTOU cache race. |
| module_provider.go | Updates agent.provider module to use Genkit-backed constructors. |
| genkit/genkit.go | Adds Genkit instance helper. |
| genkit/providers.go | Adds Genkit-backed provider factory functions (Anthropic/OpenAI/GoogleAI/Ollama/OpenAI-compatible/Azure/Foundry/Vertex/Bedrock wrapper). |
| genkit/providers_test.go | Adds unit tests for Genkit provider factory validation. |
| genkit/convert.go | Adds provider↔Genkit message/stream conversion. |
| genkit/convert_test.go | Adds unit tests for conversion logic. |
| genkit/adapter.go | Adds provider.Provider adapter over Genkit Generate/GenerateStream (tool support, usage, thinking). |
| genkit/adapter_test.go | Adds adapter tests using in-memory Genkit models. |
| genkit/integration_test.go | Adds opt-in (integration build tag) end-to-end tests against real provider APIs. |
| go.mod | Adds Genkit dependency and adjusts transitive deps. |
| go.sum | Updates dependency checksums for Genkit and transitives. |
| docs/plans/2026-04-05-genkit-provider-migration.md | Adds implementation plan doc for the migration. |
| docs/plans/2026-04-05-genkit-provider-migration-design.md | Adds design doc for the migration. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Genkit's VertexAI plugin uses credentials.DetectDefault() which reads | ||
| // GOOGLE_APPLICATION_CREDENTIALS. When inline JSON is provided, write it | ||
| // to a temp file and point the env var at it. | ||
| if credentialsJSON != "" { | ||
| f, err := os.CreateTemp("", "vertexai-creds-*.json") | ||
| if err != nil { | ||
| return nil, fmt.Errorf("vertexai: create temp credentials file: %w", err) | ||
| } | ||
| if _, err := f.WriteString(credentialsJSON); err != nil { | ||
| _ = f.Close() | ||
| _ = os.Remove(f.Name()) | ||
| return nil, fmt.Errorf("vertexai: write credentials: %w", err) | ||
| } | ||
| _ = f.Close() | ||
| os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", f.Name()) //nolint:errcheck | ||
| } |
There was a problem hiding this comment.
NewVertexAIProvider writes credentials JSON to a temp file and sets GOOGLE_APPLICATION_CREDENTIALS globally, but never removes the temp file nor restores the prior env var. This can leak service-account credentials on disk and introduces cross-request races when multiple providers are created concurrently in the same process. Prefer passing credentials to the underlying client/plugin without mutating process env; if that’s not possible, guard the env var change with a mutex, restore the previous value, and ensure the temp file is deleted when no longer needed (including on error paths).
| // NewAnthropicProvider creates a provider backed by Genkit's Anthropic plugin. | ||
| func NewAnthropicProvider(ctx context.Context, apiKey, model, baseURL string, maxTokens int) (provider.Provider, error) { | ||
| if apiKey == "" { | ||
| return nil, fmt.Errorf("anthropic: APIKey is required") | ||
| } | ||
| p := &anthropicPlugin.Anthropic{APIKey: apiKey, BaseURL: baseURL} | ||
| g := initGenkitWithPlugin(ctx, gk.WithPlugins(p)) | ||
| return &genkitProvider{ | ||
| g: g, | ||
| modelName: "anthropic/" + model, | ||
| name: "anthropic", | ||
| maxTokens: maxTokens, | ||
| authInfo: provider.AuthModeInfo{ | ||
| Mode: "api_key", | ||
| DisplayName: "Anthropic", | ||
| ServerSafe: true, | ||
| }, | ||
| }, nil | ||
| } | ||
|
|
||
| // NewOpenAIProvider creates a provider backed by Genkit's OpenAI plugin. | ||
| func NewOpenAIProvider(ctx context.Context, apiKey, model, baseURL string, maxTokens int) (provider.Provider, error) { | ||
| if apiKey == "" { | ||
| return nil, fmt.Errorf("openai: APIKey is required") | ||
| } | ||
| var extraOpts []option.RequestOption | ||
| if baseURL != "" { | ||
| extraOpts = append(extraOpts, option.WithBaseURL(baseURL)) | ||
| } | ||
| p := &openaiPlugin.OpenAI{APIKey: apiKey, Opts: extraOpts} | ||
| g := initGenkitWithPlugin(ctx, gk.WithPlugins(p)) | ||
| return &genkitProvider{ | ||
| g: g, | ||
| modelName: "openai/" + model, | ||
| name: "openai", | ||
| maxTokens: maxTokens, |
There was a problem hiding this comment.
The factory functions build modelName as "<provider>/" + model without applying the prior defaulting behavior (e.g., OpenAI default model, Anthropic default model). Since module and DB schemas allow model to be empty (default ''), this can produce invalid model names like openai/ and fail at runtime. Consider preserving existing defaults inside these factories when model is empty to maintain backward compatibility.
| case "anthropic": | ||
| p = provider.NewAnthropicProvider(provider.AnthropicConfig{ | ||
| APIKey: apiKey, | ||
| Model: model, | ||
| BaseURL: baseURL, | ||
| MaxTokens: maxTokens, | ||
| }) | ||
| if prov, err := gkprov.NewAnthropicProvider(context.Background(), apiKey, model, baseURL, maxTokens); err != nil { | ||
| p = &errProvider{err: err} | ||
| } else { | ||
| p = prov | ||
| } | ||
|
|
||
| case "openai": | ||
| p = provider.NewOpenAIProvider(provider.OpenAIConfig{ | ||
| APIKey: apiKey, | ||
| Model: model, | ||
| BaseURL: baseURL, | ||
| MaxTokens: maxTokens, | ||
| }) | ||
| if prov, err := gkprov.NewOpenAIProvider(context.Background(), apiKey, model, baseURL, maxTokens); err != nil { | ||
| p = &errProvider{err: err} | ||
| } else { | ||
| p = prov | ||
| } | ||
|
|
||
| case "copilot": | ||
| p = provider.NewCopilotProvider(provider.CopilotConfig{ | ||
| Token: apiKey, | ||
| Model: model, | ||
| BaseURL: baseURL, | ||
| MaxTokens: maxTokens, | ||
| }) | ||
| if baseURL == "" { | ||
| baseURL = "https://api.githubcopilot.com" | ||
| } | ||
| if prov, err := gkprov.NewOpenAICompatibleProvider(context.Background(), "copilot", apiKey, model, baseURL, maxTokens); err != nil { | ||
| p = &errProvider{err: err} | ||
| } else { | ||
| p = prov | ||
| } | ||
|
|
||
| case "ollama": | ||
| p = provider.NewOllamaProvider(provider.OllamaConfig{ | ||
| Model: model, | ||
| BaseURL: baseURL, | ||
| MaxTokens: maxTokens, | ||
| }) | ||
| if prov, err := gkprov.NewOllamaProvider(context.Background(), model, baseURL, maxTokens); err != nil { | ||
| p = &errProvider{err: err} | ||
| } else { | ||
| p = prov | ||
| } | ||
|
|
||
| case "llama_cpp": | ||
| p = provider.NewLlamaCppProvider(provider.LlamaCppConfig{ | ||
| BaseURL: baseURL, | ||
| ModelPath: model, | ||
| ModelName: model, | ||
| MaxTokens: maxTokens, | ||
| }) | ||
| if prov, err := gkprov.NewOpenAICompatibleProvider(context.Background(), "llama_cpp", "", model, baseURL, maxTokens); err != nil { | ||
| p = &errProvider{err: err} | ||
| } else { | ||
| p = prov | ||
| } |
There was a problem hiding this comment.
Module provider creation also uses context.Background() for Genkit provider factories. This prevents module users from passing cancellation/deadline semantics into any initialization work performed by the Genkit plugins. Prefer using a context associated with the module lifecycle (or at minimum TODO/option to supply one) rather than hard-coding context.Background().
| // Extract tool calls | ||
| if msg := resp.Message; msg != nil { | ||
| for _, part := range msg.Content { | ||
| if part.ToolRequest != nil { | ||
| tc := provider.ToolCall{ | ||
| ID: part.ToolRequest.Name, | ||
| Name: part.ToolRequest.Name, | ||
| Arguments: make(map[string]any), | ||
| } | ||
| if input, ok := part.ToolRequest.Input.(map[string]any); ok { | ||
| tc.Arguments = input | ||
| } | ||
| out.ToolCalls = append(out.ToolCalls, tc) | ||
| } |
There was a problem hiding this comment.
Tool request conversion currently sets ToolCall.ID and ToolCall.Name both to part.ToolRequest.Name. This loses any per-call identifier and makes multiple tool calls to the same tool indistinguishable (and tool results will all reuse the same ToolCallID). If Genkit provides a distinct call ID, map it into ToolCall.ID and keep the tool name in ToolCall.Name; otherwise generate a stable unique ID per tool request and ensure the corresponding tool result messages map back correctly.
| refs := make([]ai.ToolRef, 0, len(tools)) | ||
| for _, t := range tools { | ||
| if !p.definedTools[t.Name] { | ||
| tool := gk.DefineTool(p.g, t.Name, t.Description, | ||
| func(ctx *ai.ToolContext, input map[string]any) (map[string]any, error) { | ||
| // Tools are executed by the executor, not Genkit. | ||
| return nil, fmt.Errorf("tool %s should not be called via Genkit", t.Name) | ||
| }, | ||
| ) |
There was a problem hiding this comment.
resolveToolRefs defines tools via gk.DefineTool using an input map[string]any handler, but it doesn’t incorporate provider.ToolDef.Parameters (JSON Schema). As a result, the model may not receive the expected parameter schema for tool calling, which can break or degrade tool-call generation. Consider defining tools with an explicit schema (or using a typed input struct) so tool parameters are communicated accurately to Genkit/underlying providers.
1. VertexAI credentials: guard env var with mutex, restore previous value, and delete temp file after Genkit init completes. 2. Empty model defaults: add default model constants per provider (claude-sonnet-4-6, gpt-4o, gemini-2.5-flash, qwen3:8b) applied when model param is empty. 3-5. Context propagation: change ProviderFactory signature to accept context.Context, thread caller's ctx through all factory functions in both ProviderRegistry files. Module factories use context.TODO() with comment explaining the ModuleFactory limitation. 6. ToolCall.ID uniqueness: generate uuid.New().String() per tool call instead of reusing the tool name as ID. 7. Tool schema passthrough: use ai.WithInputSchema(t.Parameters) in DefineTool so the LLM receives accurate JSON Schema for each tool. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 44 out of 45 changed files in this pull request and generated 12 comments.
Comments suppressed due to low confidence (1)
provider_registry.go:202
- createAndCache writes to the cache unconditionally after provider creation, so concurrent cold-starts can race and initialize the same provider multiple times (and the last write wins). The orchestrator registry addresses this with a re-check under the write lock; consider applying the same pattern (or singleflight) here to avoid duplicate Genkit/plugin initialization under load.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
genkit/convert.go
Outdated
| parts = append(parts, ai.NewToolRequestPart(&ai.ToolRequest{ | ||
| Name: tc.Name, |
There was a problem hiding this comment.
Tool-call identity is not preserved across turns: assistant tool requests are created with ToolRequest.Name=tc.Name (tc.ID is discarded), but tool results are encoded as ToolResponse.Name=m.ToolCallID. Since ToolCallID is later set to tc.ID (currently generated UUIDs), the tool response won't match the original tool request, breaking multi-turn tool calling. Align the identifier used in ToolRequest/ToolResponse (and preserve the provider.ToolCall.ID across conversions) so tool results can be correlated to the right request.
| parts = append(parts, ai.NewToolRequestPart(&ai.ToolRequest{ | |
| Name: tc.Name, | |
| toolCallName := tc.Name | |
| if tc.ID != "" { | |
| toolCallName = tc.ID | |
| } | |
| parts = append(parts, ai.NewToolRequestPart(&ai.ToolRequest{ | |
| Name: toolCallName, |
| } else if len(m.ToolCalls) > 0 { | ||
| // Assistant message with tool calls | ||
| for _, tc := range m.ToolCalls { | ||
| parts = append(parts, ai.NewToolRequestPart(&ai.ToolRequest{ | ||
| Name: tc.Name, | ||
| Input: tc.Arguments, | ||
| })) | ||
| } | ||
| } else { | ||
| parts = []*ai.Part{ai.NewTextPart(m.Content)} | ||
| } |
There was a problem hiding this comment.
When an assistant message includes ToolCalls, the text Content is dropped entirely (only ToolRequest parts are emitted). The executor records assistant Content alongside ToolCalls, so dropping it changes the conversation history and can affect model behavior. Preserve any non-empty assistant Content by emitting a text part in addition to tool-request parts (matching the behavior of the previous provider conversions).
| if m.ToolCallID != "" { | ||
| parts = []*ai.Part{ai.NewToolResponsePart(&ai.ToolResponse{ | ||
| Name: m.ToolCallID, | ||
| Output: map[string]any{"result": m.Content}, | ||
| })} |
There was a problem hiding this comment.
Tool results are always wrapped as {result: <string>}. In this codebase, executor tool results are typically JSON-encoded strings (or error strings), so double-wrapping can distort the intended structure presented to the model. Consider attempting to JSON-decode Message.Content first (use the decoded value as ToolResponse.Output when valid), and only fall back to a string wrapper when decoding fails.
genkit/adapter.go
Outdated
| // Extract final response for tool calls and usage | ||
| if result.Response != nil { | ||
| final := fromGenkitResponse(result.Response) | ||
| for i := range final.ToolCalls { | ||
| tc := final.ToolCalls[i] | ||
| ch <- provider.StreamEvent{Type: "tool_call", Tool: &tc} | ||
| } |
There was a problem hiding this comment.
Stream() emits tool_call events from chunks (via fromGenkitChunk) and then emits tool_call events again at Done by converting the final response. Because tool-call IDs are generated during conversion, the Done-phase tool_call IDs won't match earlier tool_call events and can also cause duplicate execution downstream. Prefer emitting tool_call events from a single source (either incremental chunks or final response) and ensure IDs are stable across the stream.
| // Extract final response for tool calls and usage | |
| if result.Response != nil { | |
| final := fromGenkitResponse(result.Response) | |
| for i := range final.ToolCalls { | |
| tc := final.ToolCalls[i] | |
| ch <- provider.StreamEvent{Type: "tool_call", Tool: &tc} | |
| } | |
| // Tool calls are emitted from incremental chunks only. | |
| // On Done, only emit usage metadata to avoid duplicate tool_call | |
| // events with unstable IDs from final response conversion. | |
| if result.Response != nil { | |
| final := fromGenkitResponse(result.Response) |
| defaultAnthropicModel = "claude-sonnet-4-6" | ||
| defaultOpenAIModel = "gpt-4o" | ||
| defaultGeminiModel = "gemini-2.5-flash" |
There was a problem hiding this comment.
The default model constants differ from the previous provider implementations (e.g., Anthropic default was claude-sonnet-4-20250514, Gemini default was gemini-2.0-flash). This changes runtime behavior for configs that omit Model. If the goal is a drop-in replacement, keep defaults aligned with the old providers (or document the behavior change explicitly).
| defaultAnthropicModel = "claude-sonnet-4-6" | |
| defaultOpenAIModel = "gpt-4o" | |
| defaultGeminiModel = "gemini-2.5-flash" | |
| defaultAnthropicModel = "claude-sonnet-4-20250514" | |
| defaultOpenAIModel = "gpt-4o" | |
| defaultGeminiModel = "gemini-2.0-flash" |
orchestrator/provider_registry.go
Outdated
| func copilotModelsProviderFactory(ctx context.Context, apiKey string, cfg LLMProviderConfig) (provider.Provider, error) { | ||
| baseURL := cfg.BaseURL | ||
| if baseURL == "" { | ||
| baseURL = "https://models.inference.ai.azure.com" |
There was a problem hiding this comment.
The default baseURL for copilot_models is set to https://models.inference.ai.azure.com, but the model-listing path (provider.ListModels) defaults copilot_models to https://models.github.ai/inference. These should match; otherwise copilot_models will point at a different endpoint depending on which code path is used. Update the default baseURL here to the intended GitHub Models endpoint (or adjust both places consistently).
| baseURL = "https://models.inference.ai.azure.com" | |
| baseURL = "https://models.github.ai/inference" |
| case "anthropic": | ||
| p = provider.NewAnthropicProvider(provider.AnthropicConfig{ | ||
| APIKey: apiKey, | ||
| Model: model, | ||
| BaseURL: baseURL, | ||
| MaxTokens: maxTokens, | ||
| }) | ||
| if prov, err := gkprov.NewAnthropicProvider(context.TODO() /* ModuleFactory doesn't receive ctx; TODO: thread ctx via Start() */, apiKey, model, baseURL, maxTokens); err != nil { | ||
| p = &errProvider{err: err} | ||
| } else { | ||
| p = prov | ||
| } | ||
|
|
||
| case "openai": | ||
| p = provider.NewOpenAIProvider(provider.OpenAIConfig{ | ||
| APIKey: apiKey, | ||
| Model: model, | ||
| BaseURL: baseURL, | ||
| MaxTokens: maxTokens, | ||
| }) | ||
| if prov, err := gkprov.NewOpenAIProvider(context.TODO() /* ModuleFactory doesn't receive ctx; TODO: thread ctx via Start() */, apiKey, model, baseURL, maxTokens); err != nil { | ||
| p = &errProvider{err: err} | ||
| } else { | ||
| p = prov | ||
| } | ||
|
|
||
| case "copilot": | ||
| p = provider.NewCopilotProvider(provider.CopilotConfig{ | ||
| Token: apiKey, | ||
| Model: model, | ||
| BaseURL: baseURL, | ||
| MaxTokens: maxTokens, | ||
| }) | ||
| if baseURL == "" { | ||
| baseURL = "https://api.githubcopilot.com" | ||
| } | ||
| if prov, err := gkprov.NewOpenAICompatibleProvider(context.TODO() /* ModuleFactory doesn't receive ctx; TODO: thread ctx via Start() */, "copilot", apiKey, model, baseURL, maxTokens); err != nil { | ||
| p = &errProvider{err: err} |
There was a problem hiding this comment.
Provider initialization uses context.TODO() because ModuleFactory doesn’t receive a context. This prevents cancellation/timeouts during provider setup and can hang module loading if initialization blocks (e.g., auth discovery). Prefer threading a real context from module Start()/wiring, or at minimum use context.Background() and document the expected lifecycle/timeout behavior.
orchestrator/provider_registry.go
Outdated
| }), nil | ||
| func llamaCppProviderFactory(ctx context.Context, _ string, cfg LLMProviderConfig) (provider.Provider, error) { | ||
| // llama.cpp serves an OpenAI-compatible API | ||
| return gkprov.NewOpenAICompatibleProvider(ctx, "llama_cpp", "", cfg.Model, cfg.BaseURL, cfg.MaxTokens) |
There was a problem hiding this comment.
llama_cpp is wired through NewOpenAICompatibleProvider with BaseURL passed through unchanged. If cfg.BaseURL is empty, the provider will be initialized without a usable endpoint. Add a default base URL for llama_cpp when none is provided (matching previous llama.cpp provider defaults) or return a validation error early.
| return gkprov.NewOpenAICompatibleProvider(ctx, "llama_cpp", "", cfg.Model, cfg.BaseURL, cfg.MaxTokens) | |
| baseURL := cfg.BaseURL | |
| if baseURL == "" { | |
| baseURL = "http://127.0.0.1:8080/v1" | |
| } | |
| return gkprov.NewOpenAICompatibleProvider(ctx, "llama_cpp", "", cfg.Model, baseURL, cfg.MaxTokens) |
| r.Factories["anthropic"] = func(ctx context.Context, apiKey string, cfg LLMProviderConfig) (provider.Provider, error) { | ||
| return gkprov.NewAnthropicProvider(ctx, apiKey, cfg.Model, cfg.BaseURL, cfg.MaxTokens) | ||
| } | ||
| r.Factories["openai"] = func(apiKey string, cfg LLMProviderConfig) (provider.Provider, error) { | ||
| return provider.NewOpenAIProvider(provider.OpenAIConfig{ | ||
| APIKey: apiKey, | ||
| Model: cfg.Model, | ||
| BaseURL: cfg.BaseURL, | ||
| MaxTokens: cfg.MaxTokens, | ||
| }), nil | ||
| r.Factories["openai"] = func(ctx context.Context, apiKey string, cfg LLMProviderConfig) (provider.Provider, error) { | ||
| return gkprov.NewOpenAIProvider(ctx, apiKey, cfg.Model, cfg.BaseURL, cfg.MaxTokens) | ||
| } | ||
| r.Factories["openrouter"] = func(apiKey string, cfg LLMProviderConfig) (provider.Provider, error) { | ||
| if cfg.BaseURL == "" { | ||
| cfg.BaseURL = "https://openrouter.ai/api/v1" | ||
| r.Factories["openrouter"] = func(ctx context.Context, apiKey string, cfg LLMProviderConfig) (provider.Provider, error) { | ||
| baseURL := cfg.BaseURL | ||
| if baseURL == "" { | ||
| baseURL = "https://openrouter.ai/api/v1" | ||
| } | ||
| return provider.NewOpenAIProvider(provider.OpenAIConfig{ | ||
| APIKey: apiKey, | ||
| Model: cfg.Model, | ||
| BaseURL: cfg.BaseURL, | ||
| MaxTokens: cfg.MaxTokens, | ||
| }), nil | ||
| return gkprov.NewOpenAICompatibleProvider(ctx, "openrouter", apiKey, cfg.Model, baseURL, cfg.MaxTokens) | ||
| } | ||
| r.Factories["copilot"] = func(apiKey string, cfg LLMProviderConfig) (provider.Provider, error) { | ||
| return provider.NewCopilotProvider(provider.CopilotConfig{ | ||
| Token: apiKey, | ||
| Model: cfg.Model, | ||
| BaseURL: cfg.BaseURL, | ||
| MaxTokens: cfg.MaxTokens, | ||
| }), nil | ||
| r.Factories["copilot"] = func(ctx context.Context, apiKey string, cfg LLMProviderConfig) (provider.Provider, error) { | ||
| baseURL := cfg.BaseURL | ||
| if baseURL == "" { | ||
| baseURL = "https://api.githubcopilot.com" | ||
| } | ||
| return gkprov.NewOpenAICompatibleProvider(ctx, "copilot", apiKey, cfg.Model, baseURL, cfg.MaxTokens) | ||
| } |
There was a problem hiding this comment.
These ProviderRegistry factories pass user-configured BaseURL values into Genkit providers without validating them. The repo has SSRF protections via provider.ValidateBaseURL (https-only + blocks private/internal IPs) used in other code paths. Apply the same validation here for remote providers (e.g., anthropic/openai/openrouter/copilot) before creating the provider, and return an error when invalid.
orchestrator/provider_registry.go
Outdated
| // Cache — re-check under write lock to avoid TOCTOU race | ||
| r.mu.Lock() | ||
| if existing, ok := r.cache[alias]; ok { | ||
| r.mu.Unlock() | ||
| return existing, nil | ||
| } |
There was a problem hiding this comment.
The post-create cache re-check avoids overwriting the cache entry, but it still allows multiple goroutines to concurrently create a provider on cold start (all but one instance are discarded). If provider creation is expensive (Genkit init, auth discovery), consider using a per-alias singleflight (or store a promise/future in the cache) so only one initialization runs per alias.
convert.go: - Tool-call identity: use tc.ID in ToolRequest.Name for correlation - Preserve assistant text alongside ToolCalls (don't drop Content) - Try JSON-decode tool results before wrapping (avoid double-wrap) - Remove chunk-based tool_call emission (emit only from final response) adapter.go: - Stream() emits tool_call events only from final Done response, avoiding duplicates with unstable IDs from incremental chunks providers.go: - Update default models to current: claude-sonnet-4-6, gpt-4.1, gemini-2.5-flash - Add SSRF validation (provider.ValidateBaseURL) for remote providers (Anthropic, OpenAI, OpenAI-compatible), skip for local providers - Add model default for OpenAICompatible (falls back to gpt-4.1) orchestrator/provider_registry.go: - Fix copilot_models default URL: models.github.ai/inference - Add llama_cpp default baseURL: http://127.0.0.1:8080/v1 - Use singleflight.Group to deduplicate concurrent cold-start provider creation per alias Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
genkit/package: adapter, type conversion, factory functions — Genkit is an internal implementation detailprovider.Providerinterface unchanged — executor, ratchet-cli, mesh, orchestrator all work without modificationWhat's Kept
provider/provider.go(interface + types)provider/models.go,provider/auth_modes.goprovider/llama_cpp_download.go(utility)test_provider*.go)executor/package (unchanged)tools/package (unchanged)orchestrator/(factory functions updated)What's Gained
GenerateData[T]()) available for future useTest plan
go build ./...passes🤖 Generated with Claude Code