Skip to content

feat(vertex): fetch live publisher models#1817

Closed
llc1123 wants to merge 2 commits into
looplj:unstablefrom
llc1123:fix/vertex-live-models
Closed

feat(vertex): fetch live publisher models#1817
llc1123 wants to merge 2 commits into
looplj:unstablefrom
llc1123:fix/vertex-live-models

Conversation

@llc1123

@llc1123 llc1123 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

变更内容

  • gemini_vertex 增加 Vertex AI publisher model 在线拉取路径:有 GCP service account JSON 时调用 v1beta1/publishers/google/models
  • 无 GCP JSON 时保留现有 PublicProviderConf 静态列表 fallback。
  • 前端 Gemini Vertex 渠道增加 GCP Region、Project ID、Service Account JSON 表单,并不再要求 API key。
  • 补充 Vertex publisher model URL 构造、分页解析和模型 ID 提取测试。

验证结论

  • 直接测试 Vertex publisher model list 的 key= 路径,返回 API keys are not supported by this API,因此 API key 不能用于获取线上 publisher 模型列表。
  • 使用 OAuth token 请求会进入 Google Cloud 项目/API 权限检查,说明正确认证路径是 OAuth/service account;本机 gcloud 项目未启用 aiplatform.googleapis.com,线上请求停在 403 API disabled

已验证

  • go test ./internal/server/biz -run 'Test(BuildGeminiVertexPublisherModelsURL|FetchGeminiVertexPublisherModelsWithToken|FetchModelsGeminiPagination)$'
  • pnpm exec eslint src/features/channels/components/channels-action-dialog.tsx src/features/channels/data/schema.ts
  • pnpm exec prettier --check src/features/channels/components/channels-action-dialog.tsx src/features/channels/data/schema.ts src/locales/en/channels.json src/locales/zh-CN/channels.json
  • GIT_MASTER=1 git diff --check

备注

全量 pnpm exec tsc -b 已运行,失败在仓库既有 TypeScript 错误,未涉及本次改动文件。

llc1123 and others added 2 commits June 11, 2026 15:50
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@llc1123 llc1123 closed this Jun 11, 2026
@greptile-apps

greptile-apps Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds live Vertex AI publisher model fetching for gemini_vertex channels: when a GCP service account JSON is configured, the backend calls v1beta1/publishers/google/models via OAuth; otherwise it falls back to the existing static provider list. The frontend gains dedicated GCP Region, Project ID, and Service Account JSON fields and no longer requires an API key for this channel type.

  • Backend adds gcpCredentialsFromFetchAPIKey and buildGeminiVertexPublisherModelsURL helpers, a fetchGeminiVertexPublisherModels method with paginated OAuth fetching (up to 50 pages × 1000 models), and bypasses the tryReturnDefaultModels cache path for gemini_vertex so it always goes live.
  • Frontend encodes GCP credentials as a JSON blob in the apiKey field of the fetch-models request, hides the standard API key section for gemini_vertex, and gates "Fetch Models" availability on either stored credentials (isEdit) or a non-empty jsonData in the form.
  • Schema validation exempts gemini_vertex from the API-key requirement and mirrors the anthropic_gcp GCP field validation pattern.

Confidence Score: 3/5

The channel creation and routing paths are unaffected; the regression is confined to Fetch Models for existing gemini_vertex channels that already have stored GCP credentials.

When a user edits an existing gemini_vertex channel and changes the service account JSON, clicking Fetch Models will silently use the old stored credentials rather than the new ones — stored channel GCP credentials unconditionally overwrite the request-supplied credentials, unlike all other credential types which respect the request value when non-empty. The frontend confirmed passes channelID alongside the new apiKey for every edit-mode fetch.

internal/server/biz/model_fetcher.go — specifically the credential resolution block inside the input.ChannelID != nil branch and its interaction with the gcpCreds already set from the API key.

Important Files Changed

Filename Overview
internal/server/biz/model_fetcher.go Adds live Vertex AI publisher model fetching via OAuth/service account; contains a credential override bug where stored channel GCP creds silently overwrite request-supplied creds, and a token-refresh gap across paginated fetches.
internal/server/biz/model_fetcher_test.go New tests cover URL construction and paginated publisher model fetch with token; missing a case for custom gateway URLs without a version suffix.
frontend/src/features/channels/components/channels-action-dialog.tsx Adds GCP Region / Project ID / Service Account JSON form fields for gemini_vertex channels, hides the API key section for that type, and encodes GCP credentials as the fetch-models API key payload.
frontend/src/features/channels/data/schema.ts Adds gemini_vertex to the API-key exemption list and to the GCP credential validation block, consistent with the anthropic_gcp pattern.
frontend/src/locales/en/channels.json Adds English i18n strings for GCP Region, Project ID, and Service Account JSON form fields.
frontend/src/locales/zh-CN/channels.json Adds Chinese i18n strings for the same GCP form fields.

Sequence Diagram

sequenceDiagram
    participant FE as Frontend (Edit Dialog)
    participant GQL as GraphQL / FetchModels
    participant MF as ModelFetcher.FetchModels
    participant DB as Channel DB
    participant Google as Google Token API
    participant Vertex as Vertex AI Publisher Models

    FE->>GQL: "{ channelType, baseURL, apiKey: JSON{region,projectID,jsonData}, channelID }"
    GQL->>MF: "FetchModelsInput{...}"

    MF->>MF: "gcpCredentialsFromFetchAPIKey(apiKey) -> gcpCreds (from request)"

    alt ChannelID provided
        MF->>DB: Channel.Get(channelID)
        DB-->>MF: ch (with ch.Credentials.GCP)
        Note over MF: if ch.Credentials.GCP != nil, gcpCreds = ch.Credentials.GCP (overwrites request creds)
    end

    alt gcpCreds.JSONData present
        MF->>Google: CredentialsFromJSON + TokenSource.Token()
        Google-->>MF: access_token (string, captured once)
        loop up to 50 pages
            MF->>Vertex: "GET /v1beta1/publishers/google/models?pageToken=..."
            Vertex-->>MF: "{ publisherModels, nextPageToken }"
        end
        MF-->>GQL: "{ models }"
    else no GCP credentials
        MF->>MF: fetchGeminiVertexModels() static fallback list
        MF-->>GQL: "{ models }"
    end

    GQL-->>FE: FetchModelsPayload
Loading

Reviews (1): Last reviewed commit: "feat(frontend): collect Vertex GCP crede..." | Re-trigger Greptile

Comment on lines +316 to +321
if ch.Credentials.GCP != nil {
gcpCreds = ch.Credentials.GCP
if gcpCreds.Region == "" {
gcpCreds.Region = gcpRegionFromVertexBaseURL(input.BaseURL)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 GCP credentials from request silently overridden by stored channel credentials

When editing an existing channel, the frontend sends both channelID and the newly entered GCP credentials as apiKey. The backend extracts gcpCreds from the request's apiKey first (lines 295–298), but then unconditionally overwrites it with the stored channel credentials at line 317 whenever ch.Credentials.GCP != nil. This is asymmetric with how regular apiKey is handled — the request's apiKey is only replaced when it's empty (line 323).

The confirmed call path: handleFetchModels sends channelID: isEdit ? currentRow?.id : undefined alongside the new GCP JSON encoded in apiKey. If the channel already has stored GCP credentials, every "Fetch Models" click during an edit will use the old stored credentials, silently ignoring the new JSON the user just entered.

Comment on lines +515 to +523
creds, err := google.CredentialsFromJSON(ctx, []byte(gcpCreds.JSONData), vertexOAuthScope)
if err != nil {
return nil, fmt.Errorf("failed to parse GCP credentials: %w", err)
}

token, err := creds.TokenSource.Token()
if err != nil {
return nil, fmt.Errorf("failed to get GCP access token: %w", err)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 OAuth token captured once, bypasses automatic refresh for paginated requests

creds.TokenSource.Token() is called once and the raw AccessToken string is extracted, then passed to fetchGeminiVertexPublisherModelsWithToken for use across all pages. While Google's oauth2.TokenSource handles caching and refresh internally, extracting the raw string bypasses that mechanism. If listing models requires many pages (up to maxPages = 50) and the token is within seconds of its ~1-hour expiry when the first call is made, subsequent pages will receive 401 responses with no retry or refresh path.

Comment on lines +651 to +685
func TestBuildGeminiVertexPublisherModelsURL(t *testing.T) {
tests := []struct {
name string
baseURL string
region string
want string
}{
{
name: "global base URL stays global",
baseURL: "https://aiplatform.googleapis.com/v1",
region: "global",
want: "https://aiplatform.googleapis.com/v1beta1/publishers/google/models",
},
{
name: "regional Vertex base URL follows GCP region",
baseURL: "https://aiplatform.googleapis.com",
region: "us-central1",
want: "https://us-central1-aiplatform.googleapis.com/v1beta1/publishers/google/models",
},
{
name: "custom gateway base URL is preserved",
baseURL: "https://gateway.example.com/google/v1beta1",
region: "us-central1",
want: "https://gateway.example.com/google/v1beta1/publishers/google/models",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := buildGeminiVertexPublisherModelsURL(tt.baseURL, tt.region); got != tt.want {
t.Fatalf("buildGeminiVertexPublisherModelsURL() = %q, want %q", got, tt.want)
}
})
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Missing test case for non-versioned custom gateway URL

TestBuildGeminiVertexPublisherModelsURL covers aiplatform URLs and a versioned custom gateway (/v1beta1), but not a custom gateway URL without a version suffix (e.g., https://gateway.example.com/proxy). In that case !isAiplatformEndpoint && !strings.HasSuffix(...) falls through to the final return which appends /v1beta1/publishers/google/models. Adding a test case here would confirm the intended behavior and guard against regressions.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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