diff --git a/src/shared/key-management/openai/checker.ts b/src/shared/key-management/openai/checker.ts index e3b0806..5cec227 100644 --- a/src/shared/key-management/openai/checker.ts +++ b/src/shared/key-management/openai/checker.ts @@ -1,6 +1,6 @@ import { AxiosError } from "axios"; import { KeyCheckerBase } from "../key-checker-base"; -import type { OpenAIKey, OpenAIKeyProvider } from "./provider"; +import type { OpenAIKey, OpenAIKeyProvider, OpenAIKeyUpdate } from "./provider"; import { OpenAIModelFamily, getOpenAIModelFamily } from "../../models"; import { getAxiosInstance } from "../../network"; @@ -51,29 +51,38 @@ export class OpenAIKeyChecker extends KeyCheckerBase { this.testLiveness(key), this.maybeCreateOrganizationClones(key), ]); - const updates = { + const updates: OpenAIKeyUpdate = { modelFamilies: provisionedModels, isTrial: livenessTest.rateLimit <= 250, }; - // If the model list claims to include gpt-image-1, verify organization status with streaming test - if (provisionedModels.includes("gpt-image") || provisionedModels.includes("o3")) { - try { - const isVerifiedOrg = await this.testVerifiedOrg(key); - if (!isVerifiedOrg) { - // Only remove gpt-image from unverified orgs - they can still use o3, just not stream it - const updatedFamilies = provisionedModels.filter(family => family !== "gpt-image"); - updates.modelFamilies = updatedFamilies; - this.log.warn({ key: key.hash }, "Key's organization is not verified. Removing gpt-image-1 from available models."); - } else { - this.log.info({ key: key.hash }, "Verified organization status for key. Can use gpt-image-1 and o3 streaming."); - } - } catch (error) { - // If test fails, assume no access to be safe, but only for gpt-image + // Test organization verification status for all keys + // This is needed for GPT-5, o1, o3, and gpt-image-1 streaming restrictions + try { + const isVerifiedOrg = await this.testVerifiedOrg(key); + // Always set the organizationVerified field for all keys + updates.organizationVerified = isVerifiedOrg; + + // Only remove gpt-image from unverified orgs if they have it + if (!isVerifiedOrg && provisionedModels.includes("gpt-image")) { const updatedFamilies = provisionedModels.filter(family => family !== "gpt-image"); updates.modelFamilies = updatedFamilies; - this.log.error({ key: key.hash, error }, "Error testing organization verification status. Removing gpt-image-1 from available models."); + this.log.warn({ key: key.hash }, "Key's organization is not verified. Removing gpt-image-1 from available models."); } + + if (isVerifiedOrg) { + this.log.info({ key: key.hash }, "Verified organization status for key. Can use streaming for GPT-5, o1, o3, and gpt-image-1."); + } else { + this.log.warn({ key: key.hash }, "Key's organization is not verified. Streaming restricted for GPT-5, o1, o3, and gpt-image-1."); + } + } catch (error) { + // If test fails, assume no access to be safe + updates.organizationVerified = false; + if (provisionedModels.includes("gpt-image")) { + const updatedFamilies = provisionedModels.filter(family => family !== "gpt-image"); + updates.modelFamilies = updatedFamilies; + } + this.log.error({ key: key.hash, error }, "Error testing organization verification status. Assuming not verified for safety."); } this.updateKey(key.hash, updates); @@ -334,17 +343,17 @@ export class OpenAIKeyChecker extends KeyCheckerBase { } /** - * Tests whether the key's organization is verified by attempting to stream from the o3 model. - * Only verified organizations can stream from o3, so this is a reliable test for both - * o3 streaming and gpt-image-1 access (which also requires verified organization status). + * Tests whether the key's organization is verified by attempting to stream from the gpt-5-mini model. + * Only verified organizations can stream from GPT-5 models, so this is a reliable test for both + * GPT-5 streaming and gpt-image-1 access (which also requires verified organization status). * Returns true if the organization is verified. */ public async testVerifiedOrg(key: OpenAIKey): Promise { - this.log.info({ key: key.hash }, "Testing organization verification status via o3 streaming"); + this.log.info({ key: key.hash }, "Testing organization verification status via gpt-5-mini streaming"); try { const payload = { - model: "o3", + model: "gpt-5-nano", messages: [{ role: "user", content: "Hi" }], max_completion_tokens: 1, stream: true @@ -366,7 +375,7 @@ export class OpenAIKeyChecker extends KeyCheckerBase { if (response.status === 200) { this.log.info( { key: key.hash, status: response.status }, - `Organization is verified. Streaming o3 request succeeded with status code ${response.status}` + `Organization is verified. Streaming gpt-5-mini request succeeded with status code ${response.status}` ); return true; } @@ -379,7 +388,7 @@ export class OpenAIKeyChecker extends KeyCheckerBase { if (errorMessage.includes("organization must be verified")) { this.log.warn( { key: key.hash, status: response.status, error: errorMessage }, - "Organization is not verified: verification required for streaming o3" + "Organization is not verified: verification required for streaming gpt-5-mini" ); return false; } @@ -391,7 +400,7 @@ export class OpenAIKeyChecker extends KeyCheckerBase { if (errorMessage.includes("stream") && errorMessage.includes("unsupported_value")) { this.log.warn( { key: key.hash, status: response.status, error: errorMessage }, - "Organization is not verified: cannot stream with o3" + "Organization is not verified: cannot stream with gpt-5-mini" ); return false; } @@ -433,7 +442,7 @@ export class OpenAIKeyChecker extends KeyCheckerBase { if (errorMessage.includes("stream") && errorMessage.includes("unsupported_value")) { this.log.warn( { key: key.hash, status, error: errorMessage }, - "Organization is not verified: cannot stream with o3" + "Organization is not verified: cannot stream with gpt-5-mini" ); return false; } diff --git a/src/shared/key-management/openai/provider.ts b/src/shared/key-management/openai/provider.ts index e9f2600..353db3c 100644 --- a/src/shared/key-management/openai/provider.ts +++ b/src/shared/key-management/openai/provider.ts @@ -20,6 +20,8 @@ export interface OpenAIKey extends Key { organizationId?: string; /** Whether this is a free trial key. These are prioritized over paid keys if they can fulfill the request. */ isTrial: boolean; + /** Whether the organization associated with this key is verified. Verified organizations can use streaming for GPT-5 models and gpt-image-1. */ + organizationVerified?: boolean; /** Set when key check returns a non-transient 429. */ isOverQuota: boolean; /** @@ -146,7 +148,9 @@ export class OpenAIKeyProvider implements KeyProvider { // GPT-5 models (gpt-5, gpt-5-mini, gpt-5-nano) require verified keys for streaming const isGpt5Model = /^gpt-5(-mini|-nano)?(-\d{4}-\d{2}-\d{2})?$/.test(model) || model === "gpt-5-chat-latest"; - const isGpt5StreamingRequest = isGpt5Model && streaming; + const isO1Model = /^o1(-mini|-preview)?(-\d{4}-\d{2}-\d{2})?$/.test(model); + const isO3Model = /^o3(-mini)?(-\d{4}-\d{2}-\d{2})?$/.test(model); + const requiresVerifiedStreaming = (isGpt5Model || isO1Model || isO3Model) && streaming; // First, filter keys based on basic criteria let availableKeys = this.keys.filter( @@ -174,9 +178,8 @@ export class OpenAIKeyProvider implements KeyProvider { }); // Filter to only include keys from verified organizations - // A key is from a verified organization if our verification test didn't remove gpt-image - // This is the critical filter that ensures only verified org keys are used - const verifiedKeys = availableKeys.filter(key => key.modelFamilies.includes("gpt-image")); + // Use the organizationVerified field which is set by the key checker + const verifiedKeys = availableKeys.filter(key => key.organizationVerified === true); if (verifiedKeys.length > 0) { this.log.info( @@ -192,28 +195,28 @@ export class OpenAIKeyProvider implements KeyProvider { } } - // For GPT-5 streaming requests, we need to use only verified keys - // GPT-5 models (gpt-5, gpt-5-mini, gpt-5-nano) require verified organizations for streaming - if (isGpt5StreamingRequest) { + // For streaming requests with models that require verified organizations + // GPT-5, o1, and o3 models require verified organizations for streaming + if (requiresVerifiedStreaming) { this.log.debug( { model, keyCount: availableKeys.length, streaming }, - "Filtering keys for GPT-5 streaming request to ensure verified organization status" + "Filtering keys for streaming request to ensure verified organization status" ); // Filter to only include keys from verified organizations - // We piggyback on the existing verification logic: verified keys still have gpt-image access - const verifiedKeys = availableKeys.filter(key => key.modelFamilies.includes("gpt-image")); + // Use the organizationVerified field which is set by the key checker + const verifiedKeys = availableKeys.filter(key => key.organizationVerified === true); if (verifiedKeys.length > 0) { this.log.info( { model, totalKeys: availableKeys.length, verifiedKeys: verifiedKeys.length, streaming }, - "Using only verified organization keys for GPT-5 streaming request" + "Using only verified organization keys for streaming request" ); availableKeys = verifiedKeys; } else { this.log.warn( { model, totalKeys: availableKeys.length, streaming }, - "No verified organization keys available for GPT-5 streaming request" + "No verified organization keys available for streaming request" ); // Set availableKeys to empty array to trigger the error below availableKeys = []; @@ -221,10 +224,9 @@ export class OpenAIKeyProvider implements KeyProvider { } if (availableKeys.length === 0) { - // Provide specific error message for GPT-5 streaming requests - if (isGpt5StreamingRequest) { + if (requiresVerifiedStreaming) { throw new PaymentRequiredError( - `No verified OpenAI keys available for streaming ${model}. GPT-5 models require verified organization keys for streaming. Please disable streaming.` + "No verified OpenAI keys available for streaming GPT-5, o1, or o3 models. Only verified organizations can stream these models. Please disable streaming or contact support to verify your organization." ); } throw new PaymentRequiredError(