Files
steipete--codexbar/Sources/CodexBarCore/Providers/VertexAI/VertexAIProviderDescriptor.swift
T
2026-07-13 12:22:33 +08:00

111 lines
4.2 KiB
Swift

import Foundation
public enum VertexAIProviderDescriptor {
public static let descriptor: ProviderDescriptor = Self.makeDescriptor()
static func makeDescriptor() -> ProviderDescriptor {
ProviderDescriptor(
id: .vertexai,
metadata: ProviderMetadata(
id: .vertexai,
displayName: "Vertex AI",
sessionLabel: "Requests",
weeklyLabel: "Tokens",
opusLabel: nil,
supportsOpus: false,
supportsCredits: false,
creditsHint: "",
toggleTitle: "Show Vertex AI usage",
cliName: "vertexai",
defaultEnabled: false,
isPrimaryProvider: false,
usesAccountFallback: false,
dashboardURL: "https://console.cloud.google.com/vertex-ai",
statusPageURL: nil,
statusLinkURL: "https://status.cloud.google.com"),
branding: ProviderBranding(
iconStyle: .vertexai,
iconResourceName: "ProviderIcon-vertexai",
color: ProviderColor(red: 66 / 255, green: 133 / 255, blue: 244 / 255)),
tokenCost: ProviderTokenCostConfig(
supportsTokenCost: true,
noDataMessage: { "No Vertex AI cost data found in Claude logs. Ensure entries include Vertex metadata."
}),
fetchPlan: ProviderFetchPlan(
sourceModes: [.auto, .oauth],
pipeline: ProviderFetchPipeline(resolveStrategies: { _ in [VertexAIOAuthFetchStrategy()] })),
cli: ProviderCLIConfig(
name: "vertexai",
versionDetector: nil))
}
}
struct VertexAIOAuthFetchStrategy: ProviderFetchStrategy {
let id: String = "vertexai.oauth"
let kind: ProviderFetchKind = .oauth
func isAvailable(_ context: ProviderFetchContext) async -> Bool {
VertexAIOAuthCredentialsStore.hasCredentials(environment: context.env)
}
func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
var credentials = try await VertexAIOAuthCredentialsStore.loadForFetch(environment: context.env)
// Refresh token if expired
if credentials.needsRefresh {
credentials = try await VertexAITokenRefresher.refresh(credentials)
try VertexAIOAuthCredentialsStore.save(credentials)
}
// Fetch quota usage from Cloud Monitoring. If no data is found (e.g., no recent
// Vertex AI requests), return an empty snapshot so token costs can still display.
let usage: VertexAIUsageResponse?
do {
usage = try await VertexAIUsageFetcher.fetchUsage(
accessToken: credentials.accessToken,
projectId: credentials.projectId)
} catch VertexAIFetchError.noData {
// No quota data is fine - token costs from local logs can still be shown.
usage = nil
}
return self.makeResult(
usage: Self.mapUsage(usage, credentials: credentials),
sourceLabel: "oauth")
}
func shouldFallback(on error: Error, context _: ProviderFetchContext) -> Bool {
if error is VertexAIOAuthCredentialsError { return true }
if let fetchError = error as? VertexAIFetchError {
switch fetchError {
case .unauthorized, .forbidden:
return true
default:
return false
}
}
return false
}
private static func mapUsage(
_ response: VertexAIUsageResponse?,
credentials: VertexAIOAuthCredentials) -> UsageSnapshot
{
// Token cost is fetched separately via CostUsageScanner from local Claude logs.
// Quota usage from Cloud Monitoring is optional - we still show token costs if unavailable.
let identity = ProviderIdentitySnapshot(
providerID: .vertexai,
accountEmail: credentials.email,
accountOrganization: credentials.projectId,
loginMethod: "gcloud")
return UsageSnapshot(
primary: nil,
secondary: nil,
providerCost: nil,
updatedAt: Date(),
identity: identity)
}
}