Skip to content

feat(llm): add config-driven LLM provider package [AG-000] - #674

Draft
ShawkyZ wants to merge 2 commits into
mainfrom
feat/llm-provider-package
Draft

feat(llm): add config-driven LLM provider package [AG-000]#674
ShawkyZ wants to merge 2 commits into
mainfrom
feat/llm-provider-package

Conversation

@ShawkyZ

@ShawkyZ ShawkyZ commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

What

Adds pkg/llm, a vendor-agnostic LLM provider package (built on langchaingo), so LLM access can be shared across Snyk tools instead of living in the remy extension.

  • Contract: Provider interface + ChatRequest/ChatResponse/Message/ToolCall/… types.
  • Adapters: anthropic, openai, litellm, ollama, vertex (Gemini via genai + Claude via anthropic-sdk-go), bedrock. Retry/backoff, temperature body-strip, base-URL normalization all preserved.
  • Registry (config.go + registry.go): Resolve / New / NewFromConfig read provider selection, API keys, base URLs, and gateway headers from a configuration.Configuration.
  • Config: named env-var constants + canonical snyk_llm_* keys wired via SetSupportedEnvVars + AddAlternativeKeys. Existing env vars keep working (ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_CLOUD_*, AWS_REGION, LITELLM_*). SNYK_LLM_EXTRA_HEADERS is the new primary name; REMY_EXTRA_HEADERS retained as an alternative key. API keys are read on demand, never persisted to storage.
  • Networking: optional WithNetworkAccess threads the unauthorized HTTP transport (proxy/CA/FIPS, no Snyk auth headers) under provider clients.
  • De-branding: WithBedrockAppID sets the AWS User-Agent app id; the package ships unbranded (the SNYK_AGENTIC_FIX id is supplied by the caller).
  • Generated pkg/mocks/llm.go; /pkg/llm/ assigned to @snyk/agentic-code-security in CODEOWNERS.

Ports the full provider test suite and adds a config-driven registry test + a networking-compose test. go build/vet/test ./pkg/llm/... clean; go mod tidy stable.

⚠️ Dependency weight — needs owner sign-off

This pulls langchaingo, aws-sdk-go-v2/{config,bedrockruntime}, google.golang.org/genai, anthropics/anthropic-sdk-go, and golang.org/x/oauth2/google into GAF's graph — inherited by snyk/cli and every GAF consumer. Please confirm this is acceptable, or whether pkg/llm should be isolated (separate go.mod / build tags) before merge.

Consumer

The remy extension is migrating off its internal/provider to this package (companion PR in snyk/remy-cli-extension). CODEOWNERS routes /pkg/llm/ review to Agentic Code Security.

🤖 Generated with Claude Code


Note

High Risk
Large new surface area with third-party LLM credentials, gateway auth stripping, and heavy dependencies inherited by all GAF consumers; misconfiguration could leak keys or send code to the wrong endpoint.

Overview
Introduces pkg/llm, a shared LLM layer for GAF: a Provider contract (ChatCompletion, tools, normalized stop reasons) plus Resolve / New / NewFromConfig that read configuration.Configuration and familiar env vars (ANTHROPIC_*, OPENAI_*, GOOGLE_CLOUD_*, LITELLM_*, AWS_REGION, optional SNYK_LLM_EXTRA_HEADERS with legacy REMY_EXTRA_HEADERS).

Backends are wired through langchaingo and small custom llms.Model shims: Anthropic, OpenAI, LiteLLM (OpenAI-compatible proxy), Ollama (/api/chat with tools), Vertex (Gemini via genai, Claude on Model Garden via anthropic-sdk-go), and Bedrock. Shared behavior includes gateway header injection, placeholder keys stripped on the wire when unset, OpenAI/LiteLLM HTTPS base-URL normalization, temperature stripped from JSON bodies, per-call timeouts, and 429/529 backoff. Optional WithNetworkAccess uses the unauthorized HTTP transport (proxy/CA without Snyk auth); WithBedrockAppID brands Bedrock traffic.

Also adds FakeProvider, generated pkg/mocks/llm.go, CODEOWNERS for Agentic Code Security, and new direct deps (langchaingo, AWS Bedrock SDK, google.golang.org/genai, anthropic-sdk-go, etc.) on the root module.

Reviewed by Cursor Bugbot for commit ebf453a. Bugbot is set up for automated code reviews on this repo. Configure here.

Usage

Runnable godoc examples are in pkg/llm/example_test.go. In short:

import (
    "github.com/snyk/go-application-framework/pkg/configuration"
    "github.com/snyk/go-application-framework/pkg/llm"
)

// One-shot: resolve the provider from config (env vars / flags / Set) and build it.
config := configuration.New() // in a workflow: ictx.GetConfiguration()
provider, err := llm.NewFromConfig(config)
if err != nil { /* no provider configured */ }

resp, err := provider.ChatCompletion(ctx, &llm.ChatRequest{
    SystemPrompt: "You are a helpful assistant.",
    MaxTokens:    1024,
    Messages:     []llm.Message{{Role: llm.RoleUser, Content: "Say hello."}},
})
_ = resp.Content

Inside a GAF workflow — thread the shared transport and (for Bedrock) the app id:

provider, err := llm.NewFromConfig(ictx.GetConfiguration(),
    llm.WithNetworkAccess(ictx.GetNetworkAccess()), // proxy/CA/FIPS, no Snyk auth headers
    llm.WithLogger(ictx.GetEnhancedLogger()),
    llm.WithBedrockAppID("MY_APP"),                 // AWS User-Agent app/<id>
)

Resolve then build (when you need the provider name/model for telemetry first):

res, err := llm.Resolve(config)                 // res.Provider, res.Model
provider, err := llm.New(config, res, llm.WithNetworkAccess(na))

Tool calling — pass Tools, then echo results back as ToolResult:

resp, _ := provider.ChatCompletion(ctx, &llm.ChatRequest{
    Model:    "gpt-4o",
    Messages: []llm.Message{{Role: llm.RoleUser, Content: "Scan ./api."}},
    Tools: []llm.ToolDefinition{{
        Name: "scan", Description: "Run a Snyk scan on a path.",
        InputSchema: []byte(`{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}`),
    }},
})
if resp.StopReason == llm.StopToolUse {
    for _, call := range resp.ToolCalls { /* run call.Name(call.Input); reply with llm.ToolResult{ToolCallID: call.ID, ...} */ }
}

Provider selection — explicit via config.Set(llm.CONFIG_PROVIDER, "anthropic") (or --provider); otherwise auto-detected from ANTHROPIC_API_KEY (or ANTHROPIC_BASE_URL) then OPENAI_API_KEY. Other providers read GOOGLE_CLOUD_PROJECT/GOOGLE_CLOUD_LOCATION, AWS_REGION, LITELLM_BASE_URL/LITELLM_API_KEY, and gateway headers from SNYK_LLM_EXTRA_HEADERS.

Add pkg/llm, a vendor-agnostic LLM provider package built on langchaingo:
the Provider interface + chat/message contract types, per-vendor adapters
(anthropic, openai, litellm, ollama, vertex, bedrock), and a config-driven
registry (Resolve/New/NewFromConfig) that reads provider selection, API keys,
base URLs, and gateway headers from a configuration.Configuration.

Settings resolve from env vars via SetSupportedEnvVars + AddAlternativeKeys
(ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_CLOUD_*, AWS_REGION, LITELLM_*,
SNYK_LLM_EXTRA_HEADERS with REMY_EXTRA_HEADERS kept as an alternative key);
API keys are read on demand and never persisted to storage.

An optional NetworkAccess threads the unauthorized HTTP transport (proxy/CA/
FIPS, no Snyk auth headers) under the provider clients; WithBedrockAppID adds
the AWS User-Agent app id so the package ships vendor-neutral.

Ports the provider test suite, adds a config-driven registry test and a
networking-compose test, generates pkg/mocks/llm.go, and assigns
/pkg/llm/ to @snyk/agentic-code-security in CODEOWNERS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ShawkyZ
ShawkyZ requested review from a team as code owners July 29, 2026 18:53
@snyk-io

snyk-io Bot commented Jul 29, 2026

Copy link
Copy Markdown

Snyk checks have failed. 5 issues have been found so far.

Status Scan Engine Critical High Medium Low Total (5)
Open Source Security 1 4 0 0 5 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues
Secrets 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@snyk-io

snyk-io Bot commented Jul 29, 2026

Copy link
Copy Markdown

Snyk checks have failed. 5 issues have been found so far.

Status Scan Engine Critical High Medium Low Total (5)
Open Source Security 1 4 0 0 5 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@snyk-pr-review-bot

Copy link
Copy Markdown

PR Reviewer Guide 🔍

🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Brittle Body Manipulation 🟡 [minor]

The stripJSONBodyFields function performs a full Unmarshal into map[string]json.RawMessage to remove the temperature field. While safe for current small chat payloads, this bypasses langchaingo's internal state management. If future SDK versions start using streaming or multi-part bodies, this logic will silently pass through the unparseable body, potentially re-introducing the 'temperature is deprecated' error from providers.

func stripJSONBodyFields(req *http.Request, fields []string) error {
	raw, err := io.ReadAll(req.Body)
	_ = req.Body.Close()
	if err != nil {
		return err
	}
	out := raw
	var m map[string]json.RawMessage
	if json.Unmarshal(raw, &m) == nil {
		changed := false
		for _, f := range fields {
			if _, ok := m[f]; ok {
				delete(m, f)
				changed = true
			}
		}
		if changed {
			if b, mErr := json.Marshal(m); mErr == nil {
				out = b
			}
		}
	}
	req.Body = io.NopCloser(bytes.NewReader(out))
	req.ContentLength = int64(len(out))
	req.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(out)), nil }
	return nil
}
Implicit Provider Priority 🟡 [minor]

The Resolve function defaults to anthropic if both ANTHROPIC_API_KEY and OPENAI_API_KEY are present. While documented, this implicit priority can surprise users who set multiple keys for different tools and expect GAF to require explicit selection when ambiguous.

for _, n := range []string{"anthropic", "openai"} {
	def := providers[n]
	if !def.autoDetect {
		continue
	}
	if def.apiKeyKey != "" && config.GetString(def.apiKeyKey) != "" {
		return Resolution{Provider: n, Model: modelFor(config, def)}, nil
	}
	// A configured base URL is enough to pick the provider even without a
	// key — the gateway it points at may supply auth.
	if def.baseURLKey != "" && config.GetString(def.baseURLKey) != "" {
		return Resolution{Provider: n, Model: modelFor(config, def)}, nil
	}
}
📚 Repository Context Analyzed

This review considered 63 relevant code sections from 15 files (average relevance: 1.00)

🤖 Repository instructions applied (from AGENTS.md)

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 4 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ebf453a. Configure here.

Comment thread pkg/llm/vertex_model.go
func (m *vertexModel) generateClaude(ctx context.Context, model string, messages []llms.MessageContent, co *llms.CallOptions) (*llms.ContentResponse, error) {
if m.claude == nil {
return nil, fmt.Errorf("vertex: anthropic client not initialised (provider was configured for a Gemini model)")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vertex cross-family model override

High Severity

vertexModel builds either a Gemini (genai) or Claude (anthropic-sdk-go) client from the default model at construction, but GenerateContent picks the backend from each call’s WithModel override. A per-request model in the other family hits generateGemini or generateClaude with a nil client and fails, even though the type documents per-call model overrides.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ebf453a. Configure here.

Comment thread pkg/llm/langchain.go
ToolCallID: tr.ToolCallID,
Content: tr.Content,
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tool error flag ignored

Medium Severity

ToolResult exposes IsError, but mapping into provider messages never uses it: toLCMessage only copies ToolCallID and Content, and the Vertex Claude path always passes false into NewToolResultBlock. Failed tool runs are sent as successful results to the model.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ebf453a. Configure here.

Comment thread pkg/llm/bedrock.go
// (tool_result blocks in a single user message), like the native anthropic
// adapter — so we do NOT split tool results the way the OpenAI-family
// adapters require.
return NewLangchainAdapter("bedrock", llm), nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bedrock skips temperature strip

Medium Severity

Anthropic and OpenAI adapters strip langchaingo’s hardcoded temperature field via headerTransport, but NewBedrockAdapter wraps langchaingo Bedrock with no equivalent HTTP middleware. Claude-on-Bedrock requests can still send deprecated temperature:0 and be rejected by the model.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ebf453a. Configure here.

Comment thread pkg/llm/registry.go
// so it is not a hard requirement here.
build: func(config configuration.Configuration, res Resolution, o *options) (Provider, error) {
return NewBedrockAdapter(res.Model, config.GetString(CONFIG_AWS_REGION), o.bedrockAppID)
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bedrock ignores network transport

Medium Severity

WithNetworkAccess threads the unauthorized proxy/CA transport into other providers, but the Bedrock registry build function and NewBedrockAdapter never accept or apply that RoundTripper. Bedrock traffic uses the AWS SDK default HTTP client and bypasses corporate proxy/TLS settings used elsewhere.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ebf453a. Configure here.

@ShawkyZ
ShawkyZ marked this pull request as draft July 29, 2026 18:59
Add godoc Example functions covering the common entry points: NewFromConfig
(one-shot), option wiring (network access + Bedrock app id), Resolve+New for
telemetry, and the tool-calling round-trip. Compile-checked by go test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant