chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const {
|
||||
mockGetApiKeyWithBYOK,
|
||||
mockGetBYOKKey,
|
||||
mockGetProviderFromModel,
|
||||
mockCalculateCost,
|
||||
mockShouldBill,
|
||||
mockResolveVertex,
|
||||
} = vi.hoisted(() => ({
|
||||
mockGetApiKeyWithBYOK: vi.fn(),
|
||||
mockGetBYOKKey: vi.fn(),
|
||||
mockGetProviderFromModel: vi.fn(),
|
||||
mockCalculateCost: vi.fn(),
|
||||
mockShouldBill: vi.fn(),
|
||||
mockResolveVertex: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/api-key/byok', () => ({
|
||||
getApiKeyWithBYOK: mockGetApiKeyWithBYOK,
|
||||
getBYOKKey: mockGetBYOKKey,
|
||||
}))
|
||||
vi.mock('@/providers/utils', () => ({
|
||||
getProviderFromModel: mockGetProviderFromModel,
|
||||
calculateCost: mockCalculateCost,
|
||||
shouldBillModelUsage: mockShouldBill,
|
||||
}))
|
||||
vi.mock('@/executor/utils/vertex-credential', () => ({
|
||||
resolveVertexCredential: mockResolveVertex,
|
||||
}))
|
||||
vi.mock('@/lib/core/config/env-flags', () => ({ getCostMultiplier: () => 2 }))
|
||||
|
||||
import { computePiCost, providerApiKeyEnvVar, resolvePiModelKey } from '@/executor/handlers/pi/keys'
|
||||
|
||||
describe('providerApiKeyEnvVar', () => {
|
||||
it('maps key-based providers and rejects unsupported ones', () => {
|
||||
expect(providerApiKeyEnvVar('anthropic')).toBe('ANTHROPIC_API_KEY')
|
||||
expect(providerApiKeyEnvVar('openai')).toBe('OPENAI_API_KEY')
|
||||
expect(providerApiKeyEnvVar('vertex')).toBeNull()
|
||||
expect(providerApiKeyEnvVar('bedrock')).toBeNull()
|
||||
expect(providerApiKeyEnvVar('something-else')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('computePiCost', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('returns zero cost for BYOK keys without billing', () => {
|
||||
expect(computePiCost('claude', 100, 200, true)).toEqual({ input: 0, output: 0, total: 0 })
|
||||
expect(mockCalculateCost).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns zero cost for non-billable models', () => {
|
||||
mockShouldBill.mockReturnValue(false)
|
||||
expect(computePiCost('local-model', 100, 200, false)).toEqual({ input: 0, output: 0, total: 0 })
|
||||
expect(mockCalculateCost).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('computes billed cost with the cost multiplier', () => {
|
||||
mockShouldBill.mockReturnValue(true)
|
||||
mockCalculateCost.mockReturnValue({ input: 1, output: 2, total: 3 })
|
||||
expect(computePiCost('claude', 10, 20, false)).toEqual({ input: 1, output: 2, total: 3 })
|
||||
expect(mockCalculateCost).toHaveBeenCalledWith('claude', 10, 20, false, 2, 2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolvePiModelKey', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('resolves Vertex credentials when the provider is vertex', async () => {
|
||||
mockGetProviderFromModel.mockReturnValue('vertex')
|
||||
mockResolveVertex.mockResolvedValue('vertex-token')
|
||||
|
||||
const result = await resolvePiModelKey({
|
||||
model: 'gemini-pro',
|
||||
mode: 'local',
|
||||
userId: 'user-1',
|
||||
vertexCredential: 'cred-1',
|
||||
})
|
||||
|
||||
expect(result).toEqual({ providerId: 'vertex', apiKey: 'vertex-token', isBYOK: true })
|
||||
expect(mockGetApiKeyWithBYOK).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('local mode resolves keys through getApiKeyWithBYOK (hosted keys allowed)', async () => {
|
||||
mockGetProviderFromModel.mockReturnValue('anthropic')
|
||||
mockGetApiKeyWithBYOK.mockResolvedValue({ apiKey: 'sk-test', isBYOK: false })
|
||||
|
||||
const result = await resolvePiModelKey({
|
||||
model: 'claude',
|
||||
mode: 'local',
|
||||
workspaceId: 'ws-1',
|
||||
apiKey: 'sk-test',
|
||||
})
|
||||
|
||||
expect(result).toEqual({ providerId: 'anthropic', apiKey: 'sk-test', isBYOK: false })
|
||||
expect(mockGetApiKeyWithBYOK).toHaveBeenCalledWith('anthropic', 'claude', 'ws-1', 'sk-test')
|
||||
})
|
||||
|
||||
it('cloud mode uses the block API Key field directly as a BYOK key', async () => {
|
||||
mockGetProviderFromModel.mockReturnValue('anthropic')
|
||||
|
||||
const result = await resolvePiModelKey({
|
||||
model: 'claude',
|
||||
mode: 'cloud',
|
||||
workspaceId: 'ws-1',
|
||||
apiKey: 'sk-user',
|
||||
})
|
||||
|
||||
expect(result).toEqual({ providerId: 'anthropic', apiKey: 'sk-user', isBYOK: true })
|
||||
expect(mockGetApiKeyWithBYOK).not.toHaveBeenCalled()
|
||||
expect(mockGetBYOKKey).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('cloud mode falls back to a stored workspace key when the field is empty', async () => {
|
||||
mockGetProviderFromModel.mockReturnValue('openai')
|
||||
mockGetBYOKKey.mockResolvedValue({ apiKey: 'sk-workspace', isBYOK: true })
|
||||
|
||||
const result = await resolvePiModelKey({
|
||||
model: 'gpt-5',
|
||||
mode: 'cloud',
|
||||
workspaceId: 'ws-1',
|
||||
})
|
||||
|
||||
expect(result).toEqual({ providerId: 'openai', apiKey: 'sk-workspace', isBYOK: true })
|
||||
expect(mockGetBYOKKey).toHaveBeenCalledWith('ws-1', 'openai')
|
||||
expect(mockGetApiKeyWithBYOK).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('cloud mode falls back to a stored workspace key for xAI', async () => {
|
||||
mockGetProviderFromModel.mockReturnValue('xai')
|
||||
mockGetBYOKKey.mockResolvedValue({ apiKey: 'xai-workspace-key', isBYOK: true })
|
||||
|
||||
const result = await resolvePiModelKey({
|
||||
model: 'grok-4.5',
|
||||
mode: 'cloud',
|
||||
workspaceId: 'ws-1',
|
||||
})
|
||||
|
||||
expect(result).toEqual({ providerId: 'xai', apiKey: 'xai-workspace-key', isBYOK: true })
|
||||
expect(mockGetBYOKKey).toHaveBeenCalledWith('ws-1', 'xai')
|
||||
expect(mockGetApiKeyWithBYOK).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('cloud mode rejects when no user key is available (never a hosted key)', async () => {
|
||||
mockGetProviderFromModel.mockReturnValue('anthropic')
|
||||
mockGetBYOKKey.mockResolvedValue(null)
|
||||
|
||||
await expect(
|
||||
resolvePiModelKey({ model: 'claude', mode: 'cloud', workspaceId: 'ws-1' })
|
||||
).rejects.toThrow(/your own provider API key/)
|
||||
expect(mockGetApiKeyWithBYOK).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user