Skip to content

fix(ai): preserve plugin scenes and sanitize OpenAI chat params#547

Closed
Vme500 wants to merge 2 commits into
yikart:mainfrom
Vme500:fix/plugin-openai-chat-params
Closed

fix(ai): preserve plugin scenes and sanitize OpenAI chat params#547
Vme500 wants to merge 2 commits into
yikart:mainfrom
Vme500:fix/plugin-openai-chat-params

Conversation

@Vme500

@Vme500 Vme500 commented May 29, 2026

Copy link
Copy Markdown

Summary

This PR fixes self-hosted plugin chat issues related to model scene metadata and OpenAI chat parameter forwarding.

Changes included:

  • Preserve scenes as an array in AI model schemas.
  • Ensure model responses expose scenes as [] instead of undefined.
  • Sanitize OpenAI chat completion parameters before calling the OpenAI SDK.
  • Prevent internal request fields from being forwarded to OpenAI-compatible APIs.
  • Normalize max_tokens to max_completion_tokens for GPT-5 / gpt-5-* models.

Problem

In self-hosted local plugin mode, plugin chat requests may include internal fields such as:

  • source
  • billingGroupId
  • userId
  • userType

These fields are application-level metadata and should not be forwarded to OpenAI chat completion APIs. Forwarding them can produce 400 Unknown parameter errors.

Additionally, when model scenes is missing or undefined, plugin-side filtering such as:

model.scenes.includes("plugin")

can fail or result in an empty plugin model list.

GPT-5-compatible models may also reject max_tokens and expect max_completion_tokens instead.

Solution

This PR makes the OpenAI provider boundary stricter:

  • Only known OpenAI chat completion parameters are forwarded.
  • Internal application fields are dropped before the SDK call.
  • GPT-5 / gpt-5-* requests convert max_tokens to max_completion_tokens.
  • scenes defaults to an empty array in the model schemas.

Scope

Changed files:

  • apps/aitoearn-ai/src/config.ts
  • apps/aitoearn-ai/src/core/ai/libs/openai/openai.service.ts
  • libs/aitoearn-ai-shared/src/vos/chat.vo.ts

This PR does not add a new provider and does not modify deployment-specific files.

Safety

This PR does not include:

  • .env
  • API keys
  • local backups
  • Docker patch scripts
  • local deployment reports
  • provider-specific additions outside OpenAI

Local verification

Verified in a self-hosted local plugin deployment:

  • Plugin model list returns plugin-capable GPT models.
  • Plugin chat no longer forwards internal fields to OpenAI.
  • No source / billingGroupId unknown-parameter errors after sanitization.
  • GPT-5-compatible requests normalize max_tokens correctly.

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request updates the schema for the scenes field to default to an empty array instead of being optional, and introduces parameter sanitization and normalization in OpenaiService.createRawStream (specifically mapping max_tokens to max_completion_tokens for gpt-5 models). The reviewer identified that the sanitization logic is missing standard parameters like logprobs and top_logprobs, and that reasoning models (such as o1 and o3) should also undergo the same token normalization to prevent API errors. A code suggestion was provided to address these issues and improve TypeScript compliance.

Comment on lines +55 to +74
const allowedKeys = new Set([
'model', 'messages', 'stream', 'temperature', 'top_p', 'n', 'stop',
'max_tokens', 'max_completion_tokens', 'presence_penalty', 'frequency_penalty',
'logit_bias', 'user', 'tools', 'tool_choice', 'parallel_tool_calls',
'response_format', 'seed', 'stream_options', 'reasoning_effort',
'metadata', 'store', 'service_tier', 'prediction', 'modalities', 'audio',
])
const cleanOptions = Object.fromEntries(
Object.entries(options).filter(([key, value]) => allowedKeys.has(key) && value !== undefined),
)

const model = String(cleanOptions.model || '')
if (model === 'gpt-5' || model.startsWith('gpt-5-')) {
if (cleanOptions.max_tokens !== undefined && cleanOptions.max_completion_tokens === undefined) {
cleanOptions.max_completion_tokens = cleanOptions.max_tokens
}
delete cleanOptions.max_tokens
}

return this.openAI.chat.completions.create(cleanOptions)

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.

high

There are two main issues with the parameter sanitization and normalization logic here:

  1. Missing standard OpenAI parameters: The allowedKeys set is missing logprobs and top_logprobs. If a client requests log probabilities, these parameters will be silently stripped, leading to unexpected behavior.
  2. Incomplete reasoning model support: OpenAI's reasoning models (such as o1, o1-mini, o1-preview, and o3-mini) also do not support max_tokens and require max_completion_tokens. Restricting this normalization to only gpt-5 will cause 400 Bad Request errors when using o1 or o3 models with max_tokens.

Additionally, casting cleanOptions to any during manipulation and then casting it back to OpenAI.Chat.ChatCompletionCreateParamsStreaming when calling .create() ensures strict TypeScript compliance without compiler errors when deleting properties.

    const allowedKeys = new Set([
      'model', 'messages', 'stream', 'temperature', 'top_p', 'n', 'stop',
      'max_tokens', 'max_completion_tokens', 'presence_penalty', 'frequency_penalty',
      'logit_bias', 'logprobs', 'top_logprobs', 'user', 'tools', 'tool_choice',
      'parallel_tool_calls', 'response_format', 'seed', 'stream_options',
      'reasoning_effort', 'metadata', 'store', 'service_tier', 'prediction',
      'modalities', 'audio',
    ])
    const cleanOptions = Object.fromEntries(
      Object.entries(options).filter(([key, value]) => allowedKeys.has(key) && value !== undefined),
    ) as any

    const model = String(cleanOptions.model || '')
    const isReasoningOrGpt5 = model.startsWith('o1') || model.startsWith('o3') || model === 'gpt-5' || model.startsWith('gpt-5-')
    if (isReasoningOrGpt5) {
      if (cleanOptions.max_tokens !== undefined && cleanOptions.max_completion_tokens === undefined) {
        cleanOptions.max_completion_tokens = cleanOptions.max_tokens
      }
      delete cleanOptions.max_tokens
    }

    return this.openAI.chat.completions.create(cleanOptions as OpenAI.Chat.ChatCompletionCreateParamsStreaming)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for the review. I updated the whitelist to include logprobs and top_logprobs, expanded token normalization to o-series reasoning models using /^o\d/, and added the TypeScript cast around the cleaned options.

@Vme500 Vme500 marked this pull request as ready for review May 29, 2026 01:39
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.

2 participants