diff options
| author | Claude Sonnet 5 <noreply@anthropic.com> | 2026-07-03 22:33:22 +0000 |
|---|---|---|
| committer | Claude Sonnet 5 <noreply@anthropic.com> | 2026-07-03 22:33:22 +0000 |
| commit | 1f203a7ac0efad15ec3fc0a4c5b335ad7073a52f (patch) | |
| tree | 8047b9f63d0b1e80faf3e549b3ad21b9c1f71f1f /internal/provider/google/pricing.go | |
| parent | 767ddade57f189827fa956ff8081ca47404a4798 (diff) | |
feat(provider): add native Google Gemini API adapter (Phase 4)
Adds internal/provider/google, the second native cloud adapter (following
internal/provider/anthropic's pattern) on top of Phase 1's provider-neutral
tool-use loop, wired to a Docker-sandboxed NativeRunner under agent.type:
"google" -- a separate execution path and budget bucket from the existing
CLI-subprocess "gemini" ContainerRunner, which is untouched.
Wire-format research (the highest-risk part of this adapter): Gemini's
multi-turn function-calling shape was resolved by cross-referencing the
REST API reference's own generateContent example against the go-genai SDK's
struct tags on GitHub -- both agree on functionCall/functionResponse parts
keyed by "name" (with an optional "id" for round-tripping ToolCall.ID),
with the response fed back inside a "user"-role Content (Gemini has no
tool/function role, mirroring Anthropic's lack of one). A separate fetched
source (the function-calling guide page) was deliberately discarded as a
reference for this shape -- it documents a different, newer "Interactions
API" whose call_id/type:"function_result" structure doesn't fit the
contents/parts/candidates shape used everywhere else.
- internal/provider/google: request/response translation, systemInstruction
handling, role mapping (assistant->model, tool-results->user role),
per-model-prefix pricing table (2.5 Pro/Flash/Flash-Lite, 2.0, 1.5 tiers)
- internal/retry: IsRateLimitError additively extended for RESOURCE_EXHAUSTED
- internal/config: RunnersConfig.Google/GoogleEnabled()
- internal/cli/serve.go, run.go: runners["google"] construction mirroring
the Anthropic wiring exactly (Docker sandbox default)
- docs/api-keys-setup.md: Google marked wired-up, budget-bucket/disable/
verify guidance added matching the Anthropic section
go build/vet/test -race all pass. No live Gemini API key available in this
environment; verified via fake-httptest-server adapter tests (plain text,
tool-use round-trip, multi-turn tool-result, rate-limit error matching) plus
a Pool/NativeRunner routing test. Live E2E is a follow-up once a key is
configured, same as Phase 2's Anthropic adapter.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
Diffstat (limited to 'internal/provider/google/pricing.go')
| -rw-r--r-- | internal/provider/google/pricing.go | 60 |
1 files changed, 60 insertions, 0 deletions
diff --git a/internal/provider/google/pricing.go b/internal/provider/google/pricing.go new file mode 100644 index 0000000..55ea66f --- /dev/null +++ b/internal/provider/google/pricing.go @@ -0,0 +1,60 @@ +package google + +import "strings" + +// modelPrice is USD cost per million tokens for one model (or model family +// prefix). +type modelPrice struct { + InputPerMTok float64 + OutputPerMTok float64 +} + +// pricingTable maps a model-name *prefix* to its per-million-token pricing. +// Keyed by prefix (rather than exact model string) so dated/versioned model +// IDs (e.g. "gemini-2.5-flash-preview-09-2025") still resolve without +// needing an entry per snapshot. Edit this table as Google's pricing or +// model lineup changes — it intentionally has no other logic attached. +// +// Prices below are cached from training/research as of mid-2026 and are not +// fetched live; verify against https://ai.google.dev/gemini-api/docs/pricing +// before relying on them for real billing decisions. Two caveats specific to +// Gemini pricing that this flat per-model table does NOT model: +// - Some tiers (notably 1.5 Pro and some 2.5 Pro contexts) charge a higher +// rate once the prompt exceeds 128K/200K tokens; the price below is the +// lower/base-context rate. +// - Some Flash-tier models charge a different (higher) output rate for +// "thinking"/reasoning tokens vs. plain output tokens; the price below +// is the standard output rate, not the thinking-token rate. +var pricingTable = map[string]modelPrice{ + // Pro tier (most capable, most expensive). + "gemini-2.5-pro": {InputPerMTok: 1.25, OutputPerMTok: 10.00}, + "gemini-1.5-pro": {InputPerMTok: 1.25, OutputPerMTok: 5.00}, + + // Flash tier (balanced cost/capability; default for most tasks). + "gemini-2.5-flash-lite": {InputPerMTok: 0.10, OutputPerMTok: 0.40}, + "gemini-2.5-flash": {InputPerMTok: 0.30, OutputPerMTok: 2.50}, + "gemini-2.0-flash-lite": {InputPerMTok: 0.075, OutputPerMTok: 0.30}, + "gemini-2.0-flash": {InputPerMTok: 0.10, OutputPerMTok: 0.40}, + "gemini-1.5-flash-8b": {InputPerMTok: 0.0375, OutputPerMTok: 0.15}, + "gemini-1.5-flash": {InputPerMTok: 0.075, OutputPerMTok: 0.30}, +} + +// defaultPricing is used for unrecognized models rather than erroring — +// Flash-tier pricing is a reasonable default since it's the most commonly +// used tier for the harness's escalation ladder. +var defaultPricing = modelPrice{InputPerMTok: 0.30, OutputPerMTok: 2.50} + +// pricingFor returns the per-million-token input/output pricing for model, +// matching the longest pricingTable prefix that model starts with. Falls +// back to defaultPricing for unrecognized models. +func pricingFor(model string) (inputPerMTok, outputPerMTok float64) { + price := defaultPricing + bestLen := -1 + for prefix, p := range pricingTable { + if len(prefix) > bestLen && strings.HasPrefix(model, prefix) { + bestLen = len(prefix) + price = p + } + } + return price.InputPerMTok, price.OutputPerMTok +} |
