diff --git a/README.ja-JP.md b/README.ja-JP.md index 6ee71d4b..8938b81a 100644 --- a/README.ja-JP.md +++ b/README.ja-JP.md @@ -152,6 +152,31 @@ export OCR_USE_ANTHROPIC=true また、Claude Codeの環境変数(`ANTHROPIC_BASE_URL`、`ANTHROPIC_AUTH_TOKEN`、`ANTHROPIC_MODEL`)とも互換性があり、`~/.zshrc` / `~/.bashrc`からこれらのexportをパースします。 +### Google Vertex AIでClaudeを使用 + +OCRはGoogle Application Default Credentials(ADC)を使ってVertex AI上のClaudeを呼び出せます。Anthropic APIキーやカスタムLLM URLは不要です: + +```bash +export OCR_USE_VERTEX=true +export OCR_LLM_MODEL=claude-sonnet-4-6 +export ANTHROPIC_VERTEX_PROJECT_ID=your-gcp-project +export CLOUD_ML_REGION=global + +gcloud auth application-default login +ocr llm test +``` + +OCR専用の別名として`OCR_VERTEX_PROJECT_ID`と`OCR_VERTEX_REGION`もサポートしています。プロジェクトIDは`GOOGLE_CLOUD_PROJECT`にもフォールバックします。選択したGCPプロジェクトではVertex AI APIが有効で、対象Claudeモデルへのアクセス権が必要です。 + +同じ設定は既存の`llm`設定にも保存できます: + +```bash +ocr config set llm.use_anthropic_vertex true +ocr config set llm.vertex_project_id your-gcp-project +ocr config set llm.vertex_region global +ocr config set llm.model claude-sonnet-4-6 +``` + > **CC-Switchユーザー向けの注意**: [CC-Switch](https://github.com/farion1231/cc-switch)を[ルーティングサービス](https://www.ccswitch.io/en/docs?section=proxy&item=service)有効で使用している場合、追加設定なしで`llm.url`をCC-Switchのプロキシアドレスに向けることができます: > - **Claude**プロバイダーの場合: `llm.url`を`http://127.0.0.1:15721`に設定 > - **Codex**プロバイダーの場合: `llm.url`を`http://127.0.0.1:15721/v1`に設定 @@ -462,6 +487,9 @@ OCRは4層の優先度チェーンを使ってレビュールールを解決し | `llm.auth_header` | string | Anthropicのみ:`x-api-key` \| `authorization` | | `llm.model` | string | `claude-opus-4-6` | | `llm.use_anthropic` | boolean | `true` \| `false` | +| `llm.use_anthropic_vertex` | boolean | Google Vertex AIでClaudeを使用 | +| `llm.vertex_project_id` | string | Google CloudプロジェクトID | +| `llm.vertex_region` | string | Vertex AIリージョン | | `language` | string | 任意の言語名、例:`English`、`Chinese`(デフォルト:`English`) | | `telemetry.enabled` | boolean | `true` \| `false` | | `telemetry.exporter` | string | `console` \| `otlp` | @@ -479,6 +507,9 @@ OCRは4層の優先度チェーンを使ってレビュールールを解決し | `OCR_LLM_AUTH_HEADER` | Anthropic認証ヘッダー(`x-api-key`または`authorization`) | | `OCR_LLM_MODEL` | モデル名 | | `OCR_USE_ANTHROPIC` | `true` = Anthropic、`false` = OpenAI | +| `OCR_USE_VERTEX` | Vertex AIモードを有効化 | +| `OCR_VERTEX_PROJECT_ID` | Google CloudプロジェクトID。`ANTHROPIC_VERTEX_PROJECT_ID`と`GOOGLE_CLOUD_PROJECT`もサポート | +| `OCR_VERTEX_REGION` | Vertex AIリージョン。`CLOUD_ML_REGION`もサポート | ## テレメトリー diff --git a/README.ko-KR.md b/README.ko-KR.md index 8e0f76d3..9219dd64 100644 --- a/README.ko-KR.md +++ b/README.ko-KR.md @@ -152,6 +152,31 @@ export OCR_USE_ANTHROPIC=true Claude Code 환경 변수(`ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_MODEL`)와도 호환되며, `~/.zshrc` / `~/.bashrc`의 export도 파싱합니다. +### Google Vertex AI에서 Claude 사용 + +OCR은 Google Application Default Credentials(ADC)를 사용해 Vertex AI의 Claude를 호출할 수 있습니다. Anthropic API key나 custom LLM URL은 필요하지 않습니다. + +```bash +export OCR_USE_VERTEX=true +export OCR_LLM_MODEL=claude-sonnet-4-6 +export ANTHROPIC_VERTEX_PROJECT_ID=your-gcp-project +export CLOUD_ML_REGION=global + +gcloud auth application-default login +ocr llm test +``` + +OCR 전용 alias인 `OCR_VERTEX_PROJECT_ID`와 `OCR_VERTEX_REGION`도 지원합니다. project ID는 `GOOGLE_CLOUD_PROJECT`로도 fallback됩니다. 선택한 GCP project는 Vertex AI API가 활성화되어 있어야 하며 대상 Claude model에 접근할 수 있어야 합니다. + +같은 설정은 기존 `llm` 설정에도 저장할 수 있습니다. + +```bash +ocr config set llm.use_anthropic_vertex true +ocr config set llm.vertex_project_id your-gcp-project +ocr config set llm.vertex_region global +ocr config set llm.model claude-sonnet-4-6 +``` + > **CC-Switch 사용자 참고**: [CC-Switch](https://github.com/farion1231/cc-switch)를 [routing service](https://www.ccswitch.io/en/docs?section=proxy&item=service)와 함께 사용한다면, 추가 설정 없이 `llm.url`을 CC-Switch proxy 주소로 지정할 수 있습니다. > - **Claude** provider: `llm.url`을 `http://127.0.0.1:15721`로 설정 > - **Codex** provider: `llm.url`을 `http://127.0.0.1:15721/v1`로 설정 @@ -420,6 +445,9 @@ Config file: `~/.opencodereview/config.json` | `llm.auth_header` | string | Anthropic only: `x-api-key` \| `authorization` | | `llm.model` | string | `claude-opus-4-6` | | `llm.use_anthropic` | boolean | `true` \| `false` | +| `llm.use_anthropic_vertex` | boolean | Google Vertex AI에서 Claude 사용 | +| `llm.vertex_project_id` | string | Google Cloud project ID | +| `llm.vertex_region` | string | Vertex AI region | | `language` | string | 임의의 언어 이름, 예: `English`, `Chinese` (기본값: `English`) | | `telemetry.enabled` | boolean | `true` \| `false` | | `telemetry.exporter` | string | `console` \| `otlp` | @@ -437,6 +465,9 @@ Config file: `~/.opencodereview/config.json` | `OCR_LLM_AUTH_HEADER` | Anthropic auth header (`x-api-key` 또는 `authorization`) | | `OCR_LLM_MODEL` | Model name | | `OCR_USE_ANTHROPIC` | `true` = Anthropic, `false` = OpenAI | +| `OCR_USE_VERTEX` | Vertex AI 모드 활성화 | +| `OCR_VERTEX_PROJECT_ID` | Google Cloud project ID. `ANTHROPIC_VERTEX_PROJECT_ID`와 `GOOGLE_CLOUD_PROJECT`도 지원 | +| `OCR_VERTEX_REGION` | Vertex AI region. `CLOUD_ML_REGION`도 지원 | ## Telemetry diff --git a/README.md b/README.md index 5a614b2b..5f0e0e1d 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,31 @@ export OCR_USE_ANTHROPIC=true It is also compatible with Claude Code environment variables (`ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_MODEL`) and parses `~/.zshrc` / `~/.bashrc` for those exports. +### Claude on Google Vertex AI + +OCR can use Claude through Vertex AI with Google Application Default Credentials (ADC). No Anthropic API key or custom LLM URL is required: + +```bash +export OCR_USE_VERTEX=true +export OCR_LLM_MODEL=claude-sonnet-4-6 +export ANTHROPIC_VERTEX_PROJECT_ID=your-gcp-project +export CLOUD_ML_REGION=global + +gcloud auth application-default login +ocr llm test +``` + +`OCR_VERTEX_PROJECT_ID` and `OCR_VERTEX_REGION` are supported as OCR-specific aliases. `GOOGLE_CLOUD_PROJECT` is also accepted as a project ID fallback. The selected GCP project must have the Vertex AI API enabled and access to the requested Claude model. + +The same setup can be stored in the legacy `llm` configuration: + +```bash +ocr config set llm.use_anthropic_vertex true +ocr config set llm.vertex_project_id your-gcp-project +ocr config set llm.vertex_region global +ocr config set llm.model claude-sonnet-4-6 +``` + > **Note for CC-Switch Users**: If you are using [CC-Switch](https://github.com/farion1231/cc-switch) with [routing service](https://www.ccswitch.io/en/docs?section=proxy&item=service) enabled, you can point `llm.url` to the CC-Switch proxy address without additional configuration: > - For **Claude** provider: set `llm.url` to `http://127.0.0.1:15721` > - For **Codex** provider: set `llm.url` to `http://127.0.0.1:15721/v1` @@ -464,6 +489,9 @@ Config file: `~/.opencodereview/config.json` | `llm.auth_header` | string | Anthropic only: `x-api-key` \| `authorization` | | `llm.model` | string | `claude-opus-4-6` | | `llm.use_anthropic` | boolean | `true` \| `false` | +| `llm.use_anthropic_vertex` | boolean | Use Claude through Google Vertex AI | +| `llm.vertex_project_id` | string | Google Cloud project ID | +| `llm.vertex_region` | string | Vertex AI region | | `language` | string | Any language name, e.g. `English`, `Chinese` (default: `English`) | | `telemetry.enabled` | boolean | `true` \| `false` | | `telemetry.exporter` | string | `console` \| `otlp` | @@ -481,6 +509,9 @@ Environment variables take precedence over the config file. | `OCR_LLM_AUTH_HEADER` | Anthropic auth header (`x-api-key` or `authorization`) | | `OCR_LLM_MODEL` | Model name | | `OCR_USE_ANTHROPIC` | `true` = Anthropic, `false` = OpenAI | +| `OCR_USE_VERTEX` | Enable Vertex AI mode | +| `OCR_VERTEX_PROJECT_ID` | Google Cloud project ID; aliases `ANTHROPIC_VERTEX_PROJECT_ID` and `GOOGLE_CLOUD_PROJECT` are supported | +| `OCR_VERTEX_REGION` | Vertex AI region; `CLOUD_ML_REGION` is also supported | ## Telemetry diff --git a/README.ru-RU.md b/README.ru-RU.md index 1265618d..4b4f3541 100644 --- a/README.ru-RU.md +++ b/README.ru-RU.md @@ -152,6 +152,31 @@ export OCR_USE_ANTHROPIC=true Инструмент также совместим с переменными окружения Claude Code (`ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_MODEL`) и разбирает `~/.zshrc` / `~/.bashrc` в поисках соответствующих export'ов. +### Claude через Google Vertex AI + +OCR может вызывать Claude через Vertex AI с помощью Google Application Default Credentials (ADC). Ключ Anthropic API и пользовательский LLM URL не требуются: + +```bash +export OCR_USE_VERTEX=true +export OCR_LLM_MODEL=claude-sonnet-4-6 +export ANTHROPIC_VERTEX_PROJECT_ID=your-gcp-project +export CLOUD_ML_REGION=global + +gcloud auth application-default login +ocr llm test +``` + +Также поддерживаются OCR-алиасы `OCR_VERTEX_PROJECT_ID` и `OCR_VERTEX_REGION`. Для project ID также используется fallback на `GOOGLE_CLOUD_PROJECT`. В выбранном GCP-проекте должен быть включён Vertex AI API и доступ к нужной модели Claude. + +Эту же настройку можно сохранить в существующей секции `llm`: + +```bash +ocr config set llm.use_anthropic_vertex true +ocr config set llm.vertex_project_id your-gcp-project +ocr config set llm.vertex_region global +ocr config set llm.model claude-sonnet-4-6 +``` + > **Примечание для пользователей CC-Switch**: если вы используете [CC-Switch](https://github.com/farion1231/cc-switch) с включённым [routing service](https://www.ccswitch.io/en/docs?section=proxy&item=service), можно указать в `llm.url` адрес прокси CC-Switch без дополнительной настройки: > - для провайдера **Claude**: установите `llm.url` в `http://127.0.0.1:15721` > - для провайдера **Codex**: установите `llm.url` в `http://127.0.0.1:15721/v1` @@ -464,6 +489,9 @@ OCR разрешает правила ревью по цепочке приор | `llm.auth_header` | string | Только для Anthropic: `x-api-key` \| `authorization` | | `llm.model` | string | `claude-opus-4-6` | | `llm.use_anthropic` | boolean | `true` \| `false` | +| `llm.use_anthropic_vertex` | boolean | Использовать Claude через Google Vertex AI | +| `llm.vertex_project_id` | string | ID проекта Google Cloud | +| `llm.vertex_region` | string | Регион Vertex AI | | `language` | string | Любое название языка, например `English`, `Chinese` (по умолчанию: `English`) | | `telemetry.enabled` | boolean | `true` \| `false` | | `telemetry.exporter` | string | `console` \| `otlp` | @@ -481,6 +509,9 @@ OCR разрешает правила ревью по цепочке приор | `OCR_LLM_AUTH_HEADER` | Заголовок авторизации Anthropic (`x-api-key` или `authorization`) | | `OCR_LLM_MODEL` | Имя модели | | `OCR_USE_ANTHROPIC` | `true` = Anthropic, `false` = OpenAI | +| `OCR_USE_VERTEX` | Включить режим Vertex AI | +| `OCR_VERTEX_PROJECT_ID` | ID проекта Google Cloud; также поддерживаются `ANTHROPIC_VERTEX_PROJECT_ID` и `GOOGLE_CLOUD_PROJECT` | +| `OCR_VERTEX_REGION` | Регион Vertex AI; также поддерживается `CLOUD_ML_REGION` | ## Телеметрия diff --git a/README.zh-CN.md b/README.zh-CN.md index 9025e95c..3e0799b6 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -152,6 +152,31 @@ export OCR_USE_ANTHROPIC=true 同时兼容了 Claude Code 环境变量(`ANTHROPIC_BASE_URL`、`ANTHROPIC_AUTH_TOKEN`、`ANTHROPIC_MODEL`),并解析 `~/.zshrc` / `~/.bashrc` 中的相关导出。 +### 通过 Google Vertex AI 使用 Claude + +OCR 可以通过 Google 应用默认凭据(ADC)调用 Vertex AI 上的 Claude,不需要 Anthropic API Key 或自定义 LLM URL: + +```bash +export OCR_USE_VERTEX=true +export OCR_LLM_MODEL=claude-sonnet-4-6 +export ANTHROPIC_VERTEX_PROJECT_ID=your-gcp-project +export CLOUD_ML_REGION=global + +gcloud auth application-default login +ocr llm test +``` + +也可以使用 OCR 专用别名 `OCR_VERTEX_PROJECT_ID` 和 `OCR_VERTEX_REGION`。项目 ID 还会回退读取 `GOOGLE_CLOUD_PROJECT`。所选 GCP 项目必须启用 Vertex AI API,并具有目标 Claude 模型的访问权限。 + +同样可以写入已有的 `llm` 配置: + +```bash +ocr config set llm.use_anthropic_vertex true +ocr config set llm.vertex_project_id your-gcp-project +ocr config set llm.vertex_region global +ocr config set llm.model claude-sonnet-4-6 +``` + > **CC-Switch 用户特别提醒**:如果你使用 [CC-Switch](https://github.com/farion1231/cc-switch) 并开启了[路由服务](https://www.ccswitch.io/zh/docs?section=proxy&item=service),可以将 `llm.url` 配置成 CC-Switch 启动的代理地址,无需额外配置: > - 如果路由的是 **Claude** 供应商:设置 `llm.url` 为 `http://127.0.0.1:15721` > - 如果路由的是 **Codex** 供应商:设置 `llm.url` 为 `http://127.0.0.1:15721/v1` @@ -452,6 +477,9 @@ OCR 通过四层优先级链解析评审规则。每层采用首次匹配原则 | `llm.auth_header` | string | 仅 Anthropic:`x-api-key` \| `authorization` | | `llm.model` | string | `claude-opus-4-6` | | `llm.use_anthropic` | boolean | `true` \| `false` | +| `llm.use_anthropic_vertex` | boolean | 通过 Google Vertex AI 使用 Claude | +| `llm.vertex_project_id` | string | Google Cloud 项目 ID | +| `llm.vertex_region` | string | Vertex AI 区域 | | `language` | string | 任意语言名称,例如 `English`、`Chinese`(默认:`English`) | | `telemetry.enabled` | boolean | `true` \| `false` | | `telemetry.exporter` | string | `console` \| `otlp` | @@ -469,6 +497,9 @@ OCR 通过四层优先级链解析评审规则。每层采用首次匹配原则 | `OCR_LLM_AUTH_HEADER` | Anthropic 认证头(`x-api-key` 或 `authorization`) | | `OCR_LLM_MODEL` | 模型名称 | | `OCR_USE_ANTHROPIC` | `true` = Anthropic,`false` = OpenAI | +| `OCR_USE_VERTEX` | 启用 Vertex AI 模式 | +| `OCR_VERTEX_PROJECT_ID` | Google Cloud 项目 ID;也支持 `ANTHROPIC_VERTEX_PROJECT_ID` 和 `GOOGLE_CLOUD_PROJECT` | +| `OCR_VERTEX_REGION` | Vertex AI 区域;也支持 `CLOUD_ML_REGION` | ## 遥测 diff --git a/cmd/opencodereview/config_cmd.go b/cmd/opencodereview/config_cmd.go index 6f2c3b1b..1ee342c1 100644 --- a/cmd/opencodereview/config_cmd.go +++ b/cmd/opencodereview/config_cmd.go @@ -103,12 +103,15 @@ type Config struct { } type LlmConfig struct { - URL string `json:"url,omitempty"` - AuthToken string `json:"auth_token,omitempty"` - AuthHeader string `json:"auth_header,omitempty"` - Model string `json:"model,omitempty"` - UseAnthropic *bool `json:"use_anthropic,omitempty"` // nil = default true; false = OpenAI protocol - ExtraBody map[string]any `json:"extra_body,omitempty"` + URL string `json:"url,omitempty"` + AuthToken string `json:"auth_token,omitempty"` + AuthHeader string `json:"auth_header,omitempty"` + Model string `json:"model,omitempty"` + UseAnthropic *bool `json:"use_anthropic,omitempty"` // nil = default true; false = OpenAI protocol + UseAnthropicVertex bool `json:"use_anthropic_vertex,omitempty"` + VertexProjectID string `json:"vertex_project_id,omitempty"` + VertexRegion string `json:"vertex_region,omitempty"` + ExtraBody map[string]any `json:"extra_body,omitempty"` } // TelemetryConfig holds telemetry-specific settings. @@ -218,6 +221,16 @@ func setConfigValue(cfg *Config, key, value string) error { return fmt.Errorf("invalid boolean for llm.use_anthropic: %w", err) } cfg.Llm.UseAnthropic = &b + case "llm.use_anthropic_vertex", "llm.UseAnthropicVertex": + b, err := strconv.ParseBool(value) + if err != nil { + return fmt.Errorf("invalid boolean for llm.use_anthropic_vertex: %w", err) + } + cfg.Llm.UseAnthropicVertex = b + case "llm.vertex_project_id", "llm.VertexProjectID": + cfg.Llm.VertexProjectID = value + case "llm.vertex_region", "llm.VertexRegion": + cfg.Llm.VertexRegion = value case "language", "Language": cfg.Language = value case "telemetry.enabled", "telemetry.Enabled": @@ -247,7 +260,7 @@ func setConfigValue(cfg *Config, key, value string) error { } cfg.Llm.ExtraBody = m default: - return fmt.Errorf("unknown config key: %s\nSupported keys: provider, model, providers.., custom_providers.., llm.url, llm.auth_token, llm.auth_header, llm.model, llm.use_anthropic, llm.extra_body, language, telemetry.enabled, telemetry.exporter, telemetry.otlp_endpoint, telemetry.content_logging\nProvider fields: api_key, url, protocol, model, models, auth_header, extra_body", key) + return fmt.Errorf("unknown config key: %s\nSupported keys: provider, model, providers.., custom_providers.., llm.url, llm.auth_token, llm.auth_header, llm.model, llm.use_anthropic, llm.use_anthropic_vertex, llm.vertex_project_id, llm.vertex_region, llm.extra_body, language, telemetry.enabled, telemetry.exporter, telemetry.otlp_endpoint, telemetry.content_logging\nProvider fields: api_key, url, protocol, model, models, auth_header, extra_body", key) } return nil } diff --git a/cmd/opencodereview/config_cmd_test.go b/cmd/opencodereview/config_cmd_test.go index 2c8df5be..668a0c89 100644 --- a/cmd/opencodereview/config_cmd_test.go +++ b/cmd/opencodereview/config_cmd_test.go @@ -200,6 +200,24 @@ func TestSetConfigValueProviderEntryExtraBody(t *testing.T) { } } +func TestSetConfigValueLegacyVertexFields(t *testing.T) { + cfg := &Config{} + + if err := setConfigValue(cfg, "llm.use_anthropic_vertex", "true"); err != nil { + t.Fatalf("set use_anthropic_vertex: %v", err) + } + if err := setConfigValue(cfg, "llm.vertex_project_id", "test-project"); err != nil { + t.Fatalf("set vertex_project_id: %v", err) + } + if err := setConfigValue(cfg, "llm.vertex_region", "global"); err != nil { + t.Fatalf("set vertex_region: %v", err) + } + + if !cfg.Llm.UseAnthropicVertex || cfg.Llm.VertexProjectID != "test-project" || cfg.Llm.VertexRegion != "global" { + t.Errorf("Llm Vertex config = %#v", cfg.Llm) + } +} + func TestSetConfigValueModelWithCustomProvider(t *testing.T) { cfg := &Config{ Provider: "my-gateway", diff --git a/cmd/opencodereview/flags.go b/cmd/opencodereview/flags.go index 6c3d4f00..6b84c753 100644 --- a/cmd/opencodereview/flags.go +++ b/cmd/opencodereview/flags.go @@ -296,6 +296,6 @@ Examples: ocr config set language English ocr config set telemetry.enabled true -Supported keys: provider, model, providers.., custom_providers.., llm.url, llm.auth_token, llm.auth_header, llm.model, llm.use_anthropic, llm.extra_body, language, telemetry.enabled, telemetry.exporter, telemetry.otlp_endpoint, telemetry.content_logging +Supported keys: provider, model, providers.., custom_providers.., llm.url, llm.auth_token, llm.auth_header, llm.model, llm.use_anthropic, llm.use_anthropic_vertex, llm.vertex_project_id, llm.vertex_region, llm.extra_body, language, telemetry.enabled, telemetry.exporter, telemetry.otlp_endpoint, telemetry.content_logging Provider fields: api_key, url, protocol, model, models, auth_header, extra_body`) } diff --git a/cmd/opencodereview/llm_cmd.go b/cmd/opencodereview/llm_cmd.go index 33162332..5a5674d0 100644 --- a/cmd/opencodereview/llm_cmd.go +++ b/cmd/opencodereview/llm_cmd.go @@ -59,7 +59,10 @@ func runLLMTest() error { timeout = time.Duration(task.Timeout) * time.Second } - llmClient := llm.NewLLMClient(ep) + llmClient, err := llm.NewLLMClient(context.Background(), ep) + if err != nil { + return fmt.Errorf("create LLM client: %w", err) + } messages := make([]llm.Message, 0, len(task.Messages)) for _, m := range task.Messages { diff --git a/cmd/opencodereview/review_cmd.go b/cmd/opencodereview/review_cmd.go index be68b32c..d0bb0b6b 100644 --- a/cmd/opencodereview/review_cmd.go +++ b/cmd/opencodereview/review_cmd.go @@ -95,7 +95,10 @@ func runReview(args []string) error { return fmt.Errorf("resolve LLM endpoint: %w", err) } - llmClient := llm.NewLLMClient(ep) + llmClient, err := llm.NewLLMClient(context.Background(), ep) + if err != nil { + return fmt.Errorf("create LLM client: %w", err) + } model := ep.Model gitRunner := gitcmd.New(opts.maxGitProcs) diff --git a/go.mod b/go.mod index c6dcda13..f3f4433b 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,9 @@ require ( ) require ( + cloud.google.com/go/auth v0.7.2 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.2 // indirect @@ -36,9 +39,14 @@ require ( github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/dlclark/regexp2 v1.10.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/s2a-go v0.1.7 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/invopop/jsonschema v0.14.0 // indirect github.com/lucasb-eyer/go-colorful v1.4.0 // indirect @@ -52,14 +60,21 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect + golang.org/x/crypto v0.49.0 // indirect golang.org/x/net v0.52.0 // indirect + golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.45.0 // indirect golang.org/x/text v0.35.0 // indirect + golang.org/x/time v0.5.0 // indirect + google.golang.org/api v0.189.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/grpc v1.80.0 // indirect diff --git a/go.sum b/go.sum index 291ab0b8..7b647051 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,14 @@ charm.land/bubbletea/v2 v2.0.7 h1:7qw2tTAVar7m7klOPBYfTB0mniv/RuexsYwMRNxSeL0= charm.land/bubbletea/v2 v2.0.7/go.mod h1:DGW2q8gvzHnOpMpZTORs0aySVHCox5C+2Svk0fci1qs= charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU= charm.land/lipgloss/v2 v2.0.3/go.mod h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go/auth v0.7.2 h1:uiha352VrCDMXg+yoBtaD0tUF4Kv9vrtrWPYXwutnDE= +cloud.google.com/go/auth v0.7.2/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs= +cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI= +cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/anthropics/anthropic-sdk-go v1.47.0 h1:p1F48S/5UAGK3h2NzvZP8rqKnZqB7RkyYvOEM8dOEaQ= github.com/anthropics/anthropic-sdk-go v1.47.0/go.mod h1:3EfIfmFqxH6rbiLcIP4tPFyXL/IHakx2wDG4OU+TIEI= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= @@ -18,6 +26,7 @@ github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJ github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= @@ -34,27 +43,61 @@ github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8 github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8iyZjg= @@ -73,10 +116,17 @@ github.com/pkoukk/tiktoken-go v0.1.8 h1:85ENo+3FpWgAACBaEUVp+lctuTcYUO7BtmfhlN/Q github.com/pkoukk/tiktoken-go v0.1.8/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/standard-webhooks/standard-webhooks/libraries v0.0.1 h1:uOfcYT+3QungH6tIGSVCR/Y3KJmgJiHcojJbMTPDZAI= github.com/standard-webhooks/standard-webhooks/libraries v0.0.1/go.mod h1:L1MQhA6x4dn9r007T033lsaZMv9EmBAdXyU/+EF40fo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -91,8 +141,14 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc= @@ -119,27 +175,86 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s= go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/api v0.189.0 h1:equMo30LypAkdkLMBqfeIqtyAnlyig1JSZArl4XPwdI= +google.golang.org/api v0.189.0/go.mod h1:FLWGJKb0hb+pU2j+rJqwbnsF+ym+fQs73rbJ+KAUgy8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/llm/client.go b/internal/llm/client.go index 1773282e..91e1a172 100644 --- a/internal/llm/client.go +++ b/internal/llm/client.go @@ -12,6 +12,7 @@ import ( anthropic "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/option" + anthropicvertex "github.com/anthropics/anthropic-sdk-go/vertex" openai "github.com/openai/openai-go/v3" openaiopt "github.com/openai/openai-go/v3/option" "github.com/openai/openai-go/v3/shared" @@ -185,24 +186,29 @@ type ClientConfig struct { AuthHeader string // Auth header name: "x-api-key", "authorization", or empty for protocol default Timeout time.Duration // Request timeout ExtraBody map[string]any // Vendor-specific fields merged into every request body + Vertex *VertexConfig // Google Vertex AI transport settings } // --- Factory --- // NewLLMClient creates the appropriate client based on the resolved endpoint protocol. // protocol: "anthropic" -> AnthropicClient, anything else -> OpenAIClient. -func NewLLMClient(ep ResolvedEndpoint) LLMClient { +func NewLLMClient(ctx context.Context, ep ResolvedEndpoint) (LLMClient, error) { cfg := ClientConfig{ URL: ep.URL, APIKey: ep.Token, Model: ep.Model, AuthHeader: ep.AuthHeader, ExtraBody: ep.ExtraBody, + Vertex: ep.Vertex, } if ep.Protocol == "anthropic" { - return NewAnthropicClient(cfg) + if ep.Vertex != nil { + return NewAnthropicVertexClient(ctx, cfg) + } + return NewAnthropicClient(cfg), nil } - return NewOpenAIClient(cfg) + return NewOpenAIClient(cfg), nil } // --- Token counting with tiktoken --- @@ -471,6 +477,49 @@ type AnthropicClient struct { sdk anthropic.Client } +var vertexGoogleAuthOption = anthropicvertex.WithGoogleAuth + +// NewAnthropicVertexClient creates an Anthropic client backed by Google Vertex AI. +// Google Application Default Credentials are loaded by the official Anthropic SDK. +func NewAnthropicVertexClient(ctx context.Context, cfg ClientConfig) (client *AnthropicClient, err error) { + if cfg.Vertex == nil { + return nil, fmt.Errorf("Vertex AI configuration is required") + } + if cfg.Vertex.ProjectID == "" { + return nil, fmt.Errorf("Vertex AI project ID is required") + } + if cfg.Vertex.Region == "" { + return nil, fmt.Errorf("Vertex AI region is required") + } + if cfg.Timeout <= 0 { + cfg.Timeout = 5 * time.Minute + } + + // The SDK auth helper may panic while loading ADC; surface it as a normal + // client initialization error so CLI commands can report actionable output. + defer func() { + if recovered := recover(); recovered != nil { + client = nil + err = fmt.Errorf("initialize Google Vertex AI authentication: %v", recovered) + } + }() + + vertexOption := vertexGoogleAuthOption(ctx, cfg.Vertex.Region, cfg.Vertex.ProjectID) + opts := []option.RequestOption{ + option.WithoutEnvironmentDefaults(), + option.WithMaxRetries(5), + option.WithHeader("User-Agent", userAgent("claude-vertex")), + option.WithRequestTimeout(cfg.Timeout), + option.WithHeaderDel("X-Api-Key"), + vertexOption, + } + + return &AnthropicClient{ + cfg: cfg, + sdk: anthropic.NewClient(opts...), + }, nil +} + // NewAnthropicClient creates a new Anthropic Messages API client. func NewAnthropicClient(cfg ClientConfig) *AnthropicClient { if cfg.Timeout <= 0 { diff --git a/internal/llm/client_test.go b/internal/llm/client_test.go index 2aedfa3c..fa86868b 100644 --- a/internal/llm/client_test.go +++ b/internal/llm/client_test.go @@ -7,6 +7,7 @@ import ( "testing" anthropic "github.com/anthropics/anthropic-sdk-go" + "github.com/anthropics/anthropic-sdk-go/option" ) func TestNewOpenAIClient_URLNormalization(t *testing.T) { @@ -351,5 +352,51 @@ func TestAnthropicClient_DefaultsToAuthorizationHeader(t *testing.T) { } } +func TestNewLLMClient_Vertex(t *testing.T) { + original := vertexGoogleAuthOption + t.Cleanup(func() { vertexGoogleAuthOption = original }) + + var gotRegion, gotProject string + vertexGoogleAuthOption = func(_ context.Context, region, projectID string, _ ...string) option.RequestOption { + gotRegion = region + gotProject = projectID + return option.WithBaseURL("https://vertex.example.com") + } + + client, err := NewLLMClient(context.Background(), ResolvedEndpoint{ + URL: "https://us-central1-aiplatform.googleapis.com", + Model: "claude-sonnet-4-6", + Protocol: "anthropic", + Vertex: &VertexConfig{ProjectID: "test-project", Region: "us-central1"}, + }) + if err != nil { + t.Fatalf("NewLLMClient: %v", err) + } + if _, ok := client.(*AnthropicClient); !ok { + t.Fatalf("client type = %T, want *AnthropicClient", client) + } + if gotRegion != "us-central1" || gotProject != "test-project" { + t.Errorf("Vertex auth called with region=%q project=%q", gotRegion, gotProject) + } +} + +func TestNewLLMClient_VertexAuthPanicBecomesError(t *testing.T) { + original := vertexGoogleAuthOption + t.Cleanup(func() { vertexGoogleAuthOption = original }) + + vertexGoogleAuthOption = func(context.Context, string, string, ...string) option.RequestOption { + panic("application default credentials not found") + } + + _, err := NewLLMClient(context.Background(), ResolvedEndpoint{ + Model: "claude-sonnet-4-6", + Protocol: "anthropic", + Vertex: &VertexConfig{ProjectID: "test-project", Region: "us-central1"}, + }) + if err == nil { + t.Fatal("expected Vertex authentication error") + } +} + // Verify the SDK constant is accessible (compile-time check). var _ anthropic.CacheControlEphemeralParam = anthropic.NewCacheControlEphemeralParam() diff --git a/internal/llm/resolver.go b/internal/llm/resolver.go index 5e2432c3..762330fc 100644 --- a/internal/llm/resolver.go +++ b/internal/llm/resolver.go @@ -18,6 +18,13 @@ type ResolvedEndpoint struct { AuthHeader string // Anthropic auth header: "x-api-key" or "authorization" Source string // human-readable config source label ExtraBody map[string]any // vendor-specific request body fields + Vertex *VertexConfig // Google Vertex AI transport settings +} + +// VertexConfig holds the Google Vertex AI routing settings. +type VertexConfig struct { + ProjectID string + Region string } // Environment variable names for OCR-specific configuration. @@ -27,17 +34,23 @@ const ( envOCRLLMModel = "OCR_LLM_MODEL" envOCRLLMAuthHeader = "OCR_LLM_AUTH_HEADER" envOCRUseAnthropic = "OCR_USE_ANTHROPIC" + envOCRUseVertex = "OCR_USE_VERTEX" + envOCRVertexProject = "OCR_VERTEX_PROJECT_ID" + envOCRVertexRegion = "OCR_VERTEX_REGION" ) // Environment variable names from Claude Code configuration. const ( - envCCBaseURL = "ANTHROPIC_BASE_URL" - envCCToken = "ANTHROPIC_AUTH_TOKEN" - envCCModel = "ANTHROPIC_MODEL" + envCCBaseURL = "ANTHROPIC_BASE_URL" + envCCToken = "ANTHROPIC_AUTH_TOKEN" + envCCModel = "ANTHROPIC_MODEL" + envCCVertexProject = "ANTHROPIC_VERTEX_PROJECT_ID" + envCCVertexRegion = "CLOUD_ML_REGION" + envGoogleProject = "GOOGLE_CLOUD_PROJECT" ) -// ResolveEndpoint reads from 4 strategy sources in priority order. -// Each strategy requires all three fields (URL, Token, Model) to be non-empty. +// ResolveEndpoint reads from strategy sources in priority order. +// Non-Vertex strategies require URL, token, and model; Vertex uses ADC plus project, region, and model. // Returns the first valid strategy's result. func ResolveEndpoint(configPath string) (ResolvedEndpoint, error) { return ResolveEndpointWithModelOverride(configPath, "") @@ -54,6 +67,7 @@ func ResolveEndpointWithModelOverride(configPath, modelOverride string) (Resolve fn func() (ResolvedEndpoint, bool, error) }{ {"OCR config file", func() (ResolvedEndpoint, bool, error) { return tryOCRConfig(configPath, modelOverride) }}, + {"OCR Vertex environment", func() (ResolvedEndpoint, bool, error) { return tryOCRVertexEnv(modelOverride) }}, {"OCR environment", func() (ResolvedEndpoint, bool, error) { return tryOCREnv(modelOverride) }}, {"Claude Code environment", func() (ResolvedEndpoint, bool, error) { return tryCCEnv(modelOverride) }}, {"Shell rc file", func() (ResolvedEndpoint, bool, error) { return tryShellRC(modelOverride) }}, @@ -64,7 +78,7 @@ func ResolveEndpointWithModelOverride(configPath, modelOverride string) (Resolve if err != nil { return ResolvedEndpoint{}, fmt.Errorf("resolve %s: %w", s.name, err) } - if ok && ep.URL != "" && ep.Token != "" && ep.Model != "" { + if ok && endpointComplete(ep) { if ep.Source == "" { ep.Source = s.name } @@ -73,7 +87,37 @@ func ResolveEndpointWithModelOverride(configPath, modelOverride string) (Resolve } } - return ResolvedEndpoint{}, fmt.Errorf("no valid LLM endpoint configured; one of OCR_LLM_URL/OCR_LLM_TOKEN/OCR_LLM_MODEL, ~/.opencodereview/config.json, or ANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN/ANTHROPIC_MODEL must be set") + return ResolvedEndpoint{}, fmt.Errorf("no valid LLM endpoint configured; configure OCR LLM/Vertex environment variables, ~/.opencodereview/config.json, or ANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN/ANTHROPIC_MODEL") +} + +func endpointComplete(ep ResolvedEndpoint) bool { + if ep.Vertex != nil { + return ep.Model != "" && ep.Vertex.ProjectID != "" && ep.Vertex.Region != "" + } + return ep.URL != "" && ep.Token != "" && ep.Model != "" +} + +// tryOCRVertexEnv reads OCR-specific Vertex AI environment variables. +func tryOCRVertexEnv(modelOverride string) (ResolvedEndpoint, bool, error) { + if isTruthy(os.Getenv(envOCRUseVertex)) { + model := os.Getenv(envOCRLLMModel) + if modelOverride != "" { + model = modelOverride + } + projectID := firstNonEmpty(os.Getenv(envOCRVertexProject), os.Getenv(envCCVertexProject), os.Getenv(envGoogleProject)) + region := firstNonEmpty(os.Getenv(envOCRVertexRegion), os.Getenv(envCCVertexRegion)) + if model == "" || projectID == "" || region == "" { + return ResolvedEndpoint{}, false, nil + } + return ResolvedEndpoint{ + URL: vertexBaseURL(region), + Model: model, + Protocol: "anthropic", + Source: "OCR Vertex environment", + Vertex: &VertexConfig{ProjectID: projectID, Region: region}, + }, true, nil + } + return ResolvedEndpoint{}, false, nil } // tryOCREnv reads OCR-specific environment variables. @@ -116,12 +160,15 @@ func tryOCREnv(modelOverride string) (ResolvedEndpoint, bool, error) { // llmFileConfig represents the llm section in config.json. type llmFileConfig struct { - URL string `json:"url,omitempty"` - AuthToken string `json:"auth_token,omitempty"` - AuthHeader string `json:"auth_header,omitempty"` - Model string `json:"model,omitempty"` - UseAnthropic *bool `json:"use_anthropic,omitempty"` // pointer to distinguish unset from false - ExtraBody map[string]any `json:"extra_body,omitempty"` + URL string `json:"url,omitempty"` + AuthToken string `json:"auth_token,omitempty"` + AuthHeader string `json:"auth_header,omitempty"` + Model string `json:"model,omitempty"` + UseAnthropic *bool `json:"use_anthropic,omitempty"` // pointer to distinguish unset from false + UseAnthropicVertex bool `json:"use_anthropic_vertex,omitempty"` + VertexProjectID string `json:"vertex_project_id,omitempty"` + VertexRegion string `json:"vertex_region,omitempty"` + ExtraBody map[string]any `json:"extra_body,omitempty"` } // providerEntryConfig represents a single provider entry in config.json. @@ -295,6 +342,26 @@ func tryLegacyLlmConfig(cfg configFile, modelOverride string) (ResolvedEndpoint, if modelOverride != "" { model = modelOverride } + if cfg.Llm.UseAnthropicVertex { + if model == "" { + return ResolvedEndpoint{}, false, fmt.Errorf("llm.model is required when llm.use_anthropic_vertex is enabled") + } + if cfg.Llm.VertexProjectID == "" { + return ResolvedEndpoint{}, false, fmt.Errorf("llm.vertex_project_id is required when llm.use_anthropic_vertex is enabled") + } + if cfg.Llm.VertexRegion == "" { + return ResolvedEndpoint{}, false, fmt.Errorf("llm.vertex_region is required when llm.use_anthropic_vertex is enabled") + } + return ResolvedEndpoint{ + URL: vertexBaseURL(cfg.Llm.VertexRegion), + Model: model, + Protocol: "anthropic", + Source: "OCR config file", + ExtraBody: cfg.Llm.ExtraBody, + Vertex: &VertexConfig{ProjectID: cfg.Llm.VertexProjectID, Region: cfg.Llm.VertexRegion}, + }, true, nil + } + if cfg.Llm.URL == "" || cfg.Llm.AuthToken == "" || model == "" { return ResolvedEndpoint{}, false, nil } @@ -473,3 +540,35 @@ func ensureMessagesSuffix(rawURL string) string { } return u + "/v1/messages" } + +func isTruthy(value string) bool { + switch strings.ToLower(strings.TrimSpace(value)) { + case "true", "1", "yes": + return true + default: + return false + } +} + +func firstNonEmpty(values ...string) string { + for _, value := range values { + trimmed := strings.TrimSpace(value) + if trimmed != "" { + return trimmed + } + } + return "" +} + +func vertexBaseURL(region string) string { + switch region { + case "global": + return "https://aiplatform.googleapis.com" + case "us": + return "https://aiplatform.us.rep.googleapis.com" + case "eu": + return "https://aiplatform.eu.rep.googleapis.com" + default: + return "https://" + region + "-aiplatform.googleapis.com" + } +} diff --git a/internal/llm/resolver_test.go b/internal/llm/resolver_test.go index 7830ffd3..d8b4f055 100644 --- a/internal/llm/resolver_test.go +++ b/internal/llm/resolver_test.go @@ -252,13 +252,122 @@ func clearAllEnv(t *testing.T) { t.Helper() for _, k := range []string{ "OCR_LLM_URL", "OCR_LLM_TOKEN", "OCR_LLM_MODEL", "OCR_LLM_AUTH_HEADER", "OCR_USE_ANTHROPIC", + "OCR_USE_VERTEX", "OCR_VERTEX_PROJECT_ID", "OCR_VERTEX_REGION", "ANTHROPIC_BASE_URL", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_MODEL", + "ANTHROPIC_VERTEX_PROJECT_ID", "CLOUD_ML_REGION", "GOOGLE_CLOUD_PROJECT", "ANTHROPIC_API_KEY", "OPENAI_API_KEY", } { t.Setenv(k, "") } } +func TestResolveEndpoint_VertexEnvironment(t *testing.T) { + clearAllEnv(t) + t.Setenv("OCR_USE_VERTEX", "true") + t.Setenv("OCR_LLM_MODEL", "claude-sonnet-4-6") + t.Setenv("ANTHROPIC_VERTEX_PROJECT_ID", " test-project ") + t.Setenv("CLOUD_ML_REGION", " us-central1 ") + + ep, err := ResolveEndpoint(filepath.Join(t.TempDir(), "nonexistent.json")) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ep.Vertex == nil { + t.Fatal("Vertex config should not be nil") + } + if ep.Vertex.ProjectID != "test-project" { + t.Errorf("ProjectID = %q, want %q", ep.Vertex.ProjectID, "test-project") + } + if ep.Vertex.Region != "us-central1" { + t.Errorf("Region = %q, want %q", ep.Vertex.Region, "us-central1") + } + if ep.Protocol != "anthropic" { + t.Errorf("Protocol = %q, want %q", ep.Protocol, "anthropic") + } + if ep.Token != "" { + t.Errorf("Token = %q, want empty for Vertex ADC", ep.Token) + } + if ep.URL != "https://us-central1-aiplatform.googleapis.com" { + t.Errorf("URL = %q", ep.URL) + } +} + +func TestResolveEndpoint_IncompleteVertexEnvironmentFallsBack(t *testing.T) { + clearAllEnv(t) + t.Setenv("OCR_USE_VERTEX", "true") + t.Setenv("OCR_LLM_MODEL", "claude-sonnet-4-6") + t.Setenv("OCR_VERTEX_PROJECT_ID", "test-project") + t.Setenv("OCR_LLM_URL", "https://api.anthropic.com/v1/messages") + t.Setenv("OCR_LLM_TOKEN", "sk-test") + + ep, err := ResolveEndpoint(filepath.Join(t.TempDir(), "nonexistent.json")) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ep.Vertex != nil { + t.Fatal("incomplete Vertex environment should fall back") + } + if ep.Source != "OCR environment" { + t.Errorf("Source = %q, want %q", ep.Source, "OCR environment") + } +} + +func TestResolveEndpoint_ConfigPrecedesVertexEnvironment(t *testing.T) { + clearAllEnv(t) + t.Setenv("OCR_USE_VERTEX", "true") + t.Setenv("OCR_LLM_MODEL", "claude-sonnet-4-6") + t.Setenv("OCR_VERTEX_PROJECT_ID", "vertex-project") + t.Setenv("OCR_VERTEX_REGION", "us-central1") + + cfg := configFile{ + Provider: "openai", + Providers: map[string]providerEntryConfig{ + "openai": {APIKey: "sk-openai-test", Model: "gpt-4o"}, + }, + } + data, _ := json.Marshal(cfg) + cfgPath := filepath.Join(t.TempDir(), "config.json") + os.WriteFile(cfgPath, data, 0644) + + ep, err := ResolveEndpoint(cfgPath) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ep.Vertex != nil { + t.Fatal("config provider should take precedence over Vertex environment") + } + if ep.Source != "provider:openai" { + t.Errorf("Source = %q, want %q", ep.Source, "provider:openai") + } +} + +func TestResolveEndpoint_LegacyVertexConfig(t *testing.T) { + clearAllEnv(t) + + cfg := configFile{ + Llm: llmFileConfig{ + Model: "claude-sonnet-4-6", + UseAnthropicVertex: true, + VertexProjectID: "test-project", + VertexRegion: "global", + }, + } + data, _ := json.Marshal(cfg) + cfgPath := filepath.Join(t.TempDir(), "config.json") + os.WriteFile(cfgPath, data, 0644) + + ep, err := ResolveEndpoint(cfgPath) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ep.Vertex == nil { + t.Fatal("Vertex config should not be nil") + } + if ep.URL != "https://aiplatform.googleapis.com" { + t.Errorf("URL = %q", ep.URL) + } +} + func TestResolveEndpoint_ProviderAnthropic(t *testing.T) { clearAllEnv(t)