Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -81,19 +81,20 @@ RUN find packages -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx"
RUN --mount=type=cache,target=/root/.yarn \
yarn install --frozen-lockfile

# Install jawn dependencies
WORKDIR /app/valhalla/jawn
RUN --mount=type=cache,target=/root/.yarn \
yarn install --frozen-lockfile

# Now copy source code after dependencies are cached
WORKDIR /app
COPY packages ./packages
COPY shared ./shared
COPY valhalla ./valhalla
RUN find /app -name ".env.*" -exec rm {} \;

# Install jawn dependencies again after copying source (to ensure tsup is available)
WORKDIR /app/valhalla/jawn
RUN --mount=type=cache,target=/root/.yarn \
yarn install --frozen-lockfile

# Build jawn (dependencies already installed)
WORKDIR /app
RUN cd valhalla/jawn && yarn build


Expand Down
2 changes: 2 additions & 0 deletions docker/dockerfiles/dockerfile_web
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ FROM node:20-bookworm-slim AS builder
WORKDIR /app

COPY package.json yarn.lock ./
COPY packages/common/package.json ./packages/common/
COPY packages/cost/package.json ./packages/cost/
COPY packages/filters/package.json ./packages/filters/
COPY packages/llm-mapper/package.json ./packages/llm-mapper/
COPY packages/prompts/package.json ./packages/prompts/
COPY packages/secrets/package.json ./packages/secrets/
COPY web/package.json ./web/
COPY valhalla/jawn/package.json ./valhalla/jawn/
COPY bifrost/package.json ./bifrost/
Expand Down
42 changes: 42 additions & 0 deletions packages/__tests__/cost/testData/gpt-image-1-response.snapshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"id": "chatcmpl-test123",
"object": "chat.completion",
"created": 1757346297,
"model": "gpt-image-1",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": [
{
"type": "image",
"image": {
"url": "https://example.com/image.png"
}
}
],
"refusal": null
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 150,
"completion_tokens": 4200,
"total_tokens": 4350,
"input_tokens_details": {
"text_tokens": 50,
"image_tokens": 100
},
"completion_tokens_details": {
"reasoning_tokens": 0,
"audio_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0
}
},
"service_tier": "default",
"system_fingerprint": "fp_gpt_image_1"
}
20 changes: 20 additions & 0 deletions packages/__tests__/cost/usageProcessor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,26 @@ describe("OpenAIUsageProcessor", () => {
});
});

it("should parse gpt-image-1 response with image input tokens", async () => {
const responseData = fs.readFileSync(
path.join(__dirname, "testData", "gpt-image-1-response.snapshot"),
"utf-8"
);

const result = await processor.parse({
responseBody: responseData,
isStream: false,
model: "gpt-image-1",
});

expect(result.error).toBeNull();
expect(result.data).toEqual({
input: 50,
output: 4200,
imageInput: 100,
});
});

it("usage processing snapshot", async () => {
const testCases = [
{
Expand Down
95 changes: 95 additions & 0 deletions packages/cost/models/authors/openai/gpt-image-1/endpoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { ModelProviderName } from "../../../providers";
import type { ModelProviderConfig } from "../../../types";
import { GptImage1ModelName } from "./models";

export const endpoints = {
"gpt-image-1:openai": {
providerModelId: "gpt-image-1",
provider: "openai",
author: "openai",
pricing: [
{
threshold: 0,
input: 0.000005, // $5 per 1M text input tokens
output: 0.00004, // $40 per 1M image output tokens
image: 0.00001, // $10 per 1M image input tokens
},
],
rateLimits: {
rpm: 500,
tpm: 1000000,
},
contextLength: 128000,
maxCompletionTokens: 16384,
supportedParameters: [
"max_tokens",
"temperature",
"top_p",
"stop",
"seed",
],
ptbEnabled: true,
endpointConfigs: {
"*": {},
},
},
"gpt-image-1:azure": {
providerModelId: "gpt-image-1",
provider: "azure",
author: "openai",
pricing: [
{
threshold: 0,
input: 0.000005, // $5 per 1M text input tokens
output: 0.00004, // $40 per 1M image output tokens
image: 0.00001, // $10 per 1M image input tokens
},
],
contextLength: 128000,
maxCompletionTokens: 16384,
rateLimits: {
rpm: 300,
tpm: 500000,
},
supportedParameters: [
"max_tokens",
"temperature",
"top_p",
"stop",
"seed",
],
ptbEnabled: true,
endpointConfigs: {
"*": {},
},
},
"gpt-image-1:openrouter": {
provider: "openrouter",
author: "openai",
providerModelId: "openai/gpt-image-1",
pricing: [
{
threshold: 0,
input: 0.0000053, // $5.28/1M - worst-case: $5.00/1M (OpenAI) * 1.055
output: 0.0000422, // $42.20/1M - worst-case: $40.00/1M (OpenAI) * 1.055
image: 0.0000106, // $10.55/1M - worst-case: $10.00/1M (OpenAI) * 1.055
},
],
contextLength: 128000,
maxCompletionTokens: 16384,
supportedParameters: [
"max_tokens",
"temperature",
"top_p",
"stop",
"seed",
],
ptbEnabled: true,
priority: 3,
endpointConfigs: {
"*": {},
},
},
} satisfies Partial<
Record<`${GptImage1ModelName}:${ModelProviderName}`, ModelProviderConfig>
>;
17 changes: 17 additions & 0 deletions packages/cost/models/authors/openai/gpt-image-1/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { ModelConfig } from "../../../types";

export const models = {
"gpt-image-1": {
name: "OpenAI: GPT-Image-1",
author: "openai",
description:
"GPT-Image-1 is OpenAI's advanced image generation and editing model that combines the capabilities of image creation with precise instruction following. It offers high-fidelity image generation with support for text input and image editing workflows. The model uses a token-based pricing structure with separate rates for text input tokens, image input tokens, and image output tokens.\n\n#multimodal #image-generation",
contextLength: 128000,
maxOutputTokens: 16384,
created: "2025-01-15T00:00:00.000Z",
modality: { inputs: ["text", "image"], outputs: ["image"] },
tokenizer: "GPT",
},
} satisfies Record<string, ModelConfig>;

export type GptImage1ModelName = keyof typeof models;
4 changes: 4 additions & 0 deletions packages/cost/models/authors/openai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { models as o4Models } from "./o4/models";
import { models as gpt41Models } from "./gpt-4.1/models";
import { models as gpt5Models } from "./gpt-5/models";
import { models as ossModels } from "./oss/models";
import { models as gptImage1Models } from "./gpt-image-1/models";

// Import endpoints
import { endpoints as gpt4oEndpoints } from "./gpt-4o/endpoints";
Expand All @@ -20,6 +21,7 @@ import { endpoints as o4Endpoints } from "./o4/endpoints";
import { endpoints as gpt41Endpoints } from "./gpt-4.1/endpoints";
import { endpoints as gpt5Endpoints } from "./gpt-5/endpoints";
import { endpoints as ossEndpoints } from "./oss/endpoints";
import { endpoints as gptImage1Endpoints } from "./gpt-image-1/endpoints";

// Aggregate models
export const openaiModels = {
Expand All @@ -29,6 +31,7 @@ export const openaiModels = {
...gpt41Models,
...gpt5Models,
...ossModels,
...gptImage1Models,
} satisfies Record<string, ModelConfig>;

// Aggregate endpoints
Expand All @@ -39,4 +42,5 @@ export const openaiEndpointConfig = {
...gpt41Endpoints,
...gpt5Endpoints,
...ossEndpoints,
...gptImage1Endpoints,
} satisfies Record<string, ModelProviderConfig>;
9 changes: 8 additions & 1 deletion packages/cost/models/calculate-cost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface CostBreakdown {
videoCost: number;
webSearchCost: number;
imageCost: number;
imageInputCost: number;
requestCost: number;
totalCost: number;
}
Expand Down Expand Up @@ -60,6 +61,7 @@ export function calculateModelCostBreakdown(
videoCost: 0,
webSearchCost: 0,
imageCost: 0,
imageInputCost: 0,
requestCost: 0,
totalCost: 0,
};
Expand Down Expand Up @@ -108,11 +110,15 @@ export function calculateModelCostBreakdown(
breakdown.imageCost = modelUsage.image * pricing.image;
}

if (modelUsage.imageInput && pricing.image) {
breakdown.imageInputCost = modelUsage.imageInput * pricing.image;
}

if (requestCount > 0 && pricing.request) {
breakdown.requestCost = requestCount * pricing.request;
}

breakdown.totalCost =
breakdown.totalCost =
breakdown.inputCost +
breakdown.outputCost +
breakdown.cachedInputCost +
Expand All @@ -123,6 +129,7 @@ export function calculateModelCostBreakdown(
breakdown.videoCost +
breakdown.webSearchCost +
breakdown.imageCost +
breakdown.imageInputCost +
breakdown.requestCost;

return breakdown;
Expand Down
28 changes: 21 additions & 7 deletions packages/cost/usage/openAIUsageProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,21 +100,30 @@ export class OpenAIUsageProcessor implements IUsageProcessor {
}

const usage = parsedResponse.usage || {};

const promptTokens = usage.prompt_tokens ?? usage.input_tokens ?? 0;
const completionTokens = usage.completion_tokens ?? usage.output_tokens ?? 0;

const promptDetails = usage.prompt_tokens_details || {};
const inputTokensDetails = usage.input_tokens_details || {};
const completionDetails = usage.completion_tokens_details || {};

const cachedTokens = promptDetails.cached_tokens ?? 0;
const promptAudioTokens = promptDetails.audio_tokens ?? 0;
const completionAudioTokens = completionDetails.audio_tokens ?? 0;
const reasoningTokens = completionDetails.reasoning_tokens ?? 0;

const effectivePromptTokens = Math.max(0, promptTokens - cachedTokens - promptAudioTokens);

// Handle gpt-image-1 special token structure
const textTokens = inputTokensDetails.text_tokens ?? 0;
const imageInputTokens = inputTokensDetails.image_tokens ?? 0;

// If we have the detailed breakdown (gpt-image-1), use text_tokens
// Otherwise, fall back to the normal calculation
const effectivePromptTokens = textTokens > 0
? textTokens
: Math.max(0, promptTokens - cachedTokens - promptAudioTokens - imageInputTokens);
const effectiveCompletionTokens = Math.max(0, completionTokens - completionAudioTokens - reasoningTokens);

const modelUsage: ModelUsage = {
input: effectivePromptTokens,
output: effectiveCompletionTokens,
Expand All @@ -131,11 +140,16 @@ export class OpenAIUsageProcessor implements IUsageProcessor {
}

if (promptAudioTokens > 0 || completionAudioTokens > 0) {
// TODO: add audio output support since some models support it in the
// TODO: add audio output support since some models support it in the
// chat completions endpoint
modelUsage.audio = promptAudioTokens + completionAudioTokens;
}

// Handle gpt-image-1 image input tokens
if (imageInputTokens > 0) {
modelUsage.imageInput = imageInputTokens;
}

const rejectedTokens = completionDetails.rejected_prediction_tokens ?? 0;
const acceptedTokens = completionDetails.accepted_prediction_tokens ?? 0;
if (rejectedTokens > 0 || acceptedTokens > 0) {
Expand Down
1 change: 1 addition & 0 deletions packages/cost/usage/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export interface ModelUsage {
input: number;
output: number;
image?: number;
imageInput?: number;
cacheDetails?: {
cachedInput: number;
write5m?: number;
Expand Down
8 changes: 6 additions & 2 deletions supabase/migrations/20250731212201_provider_secret_key.sql
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ DROP FUNCTION IF EXISTS provider_keys_encrypt_secret_provider_key();
-- new.nonce
-- ),
-- 'base64') END END;

-- new.provider_secret_key = CASE WHEN new.provider_secret_key IS NULL THEN NULL ELSE
-- CASE WHEN new.key_id IS NULL THEN NULL ELSE pg_catalog.encode(
-- pgsodium.crypto_aead_det_encrypt(
Expand All @@ -30,13 +30,17 @@ DROP FUNCTION IF EXISTS provider_keys_encrypt_secret_provider_key();
-- 'base64') END END;
-- RETURN new;
-- END;

-- $$ LANGUAGE plpgsql;

-- CREATE OR REPLACE TRIGGER provider_keys_encrypt_secret_trigger_provider_key
-- BEFORE INSERT OR UPDATE ON provider_keys
-- FOR EACH ROW EXECUTE FUNCTION provider_keys_encrypt_secret_provider_key();

-- Add missing columns that should have been added earlier
ALTER TABLE provider_keys ADD COLUMN IF NOT EXISTS key_id UUID DEFAULT NULL;
ALTER TABLE provider_keys ADD COLUMN IF NOT EXISTS nonce BYTEA DEFAULT NULL;

create view public.decrypted_provider_keys_v2 as
SELECT provider_keys.id,
provider_keys.org_id,
Expand Down