feat(openai): attach Dify app_id as request metadata (opt-in)#3203
feat(openai): attach Dify app_id as request metadata (opt-in)#3203mas-sakai wants to merge 9 commits into
Conversation
Add models/llm/_metadata.py with normalize_metadata_value, build_dify_metadata, and apply_dify_metadata_if_enabled helpers, plus unit tests. Constraints match OpenAI's metadata spec (<=16 pairs, key <=64, value <=512, strings). No character pattern restriction is documented, so normalize only stringifies and truncates. The helper deliberately does not touch the store parameter; whether metadata surfaces in the Usage Dashboard is governed by the account's Stored Completions setting, which is the terminus owner's responsibility. Refs: langgenius/dify#35772, langgenius/dify-plugin-sdks#311 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s routes Insert apply_dify_metadata_if_enabled at two call sites: - _chat_generate, before client.chat.completions.create, mutating extra_model_kwargs. - _build_responses_api_params, so both _chat_generate_responses_api and _chat_generate_responses_api_stream receive metadata via the shared param builder. _build_responses_api_params now accepts an optional credentials kwarg threaded from both callers. Both routes are covered because the api_protocol credential lets users pick chat or responses per provider/model. Legacy _generate (completions.create) is intentionally out of scope. Session lookup is wrapped in a broad try/except — metadata is best-effort telemetry and must never break generation if the SDK is missing or the session context is not initialized. Refs: langgenius/dify#35772, langgenius/dify-plugin-sdks#311 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add enable_request_metadata to both model_credential_schema and provider_credential_schema as a select (enabled/disabled, default disabled), mirroring the existing api_protocol structure. Labels, options, and help text include en_US and zh_Hans. The help text documents that this plugin does not set store=true, so dashboard visibility requires the OpenAI account to have Stored Completions enabled separately. Refs: langgenius/dify#35772, langgenius/dify-plugin-sdks#311 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refs: langgenius/dify#35772, langgenius/dify-plugin-sdks#311 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Point dify_plugin at the fork branch ryuta-kobayashi-ug/dify-plugin-sdks@feat/pass-session-to-model-plugins to obtain get_current_session() in model plugins. uv.lock refreshed. Adds pytest as a dev dependency with the matching pytest config used by other plugins (testpaths=tests, pythonpath=.). Once the upstream SDK release ships, the pin will revert to the versioned dify_plugin spec. Refs: langgenius/dify#35772, langgenius/dify-plugin-sdks#311, langgenius/dify-plugin-sdks#313 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces an optional feature to attach Dify metadata (such as dify_app_id and dify_source) to OpenAI Chat Completions and Responses API requests, enabling per-app filtering in the OpenAI Usage Dashboard. The changes include a new helper module _metadata.py for metadata normalization and injection, integration within the LLM generation workflows, updated provider credential schemas, and a suite of unit tests. The review feedback highlights opportunities to improve type safety by using Any for coerced inputs, fix a logical inconsistency where falsy non-string values (like 0) are dropped, prevent overwriting existing metadata by merging dictionaries, and add a corresponding unit test.
…adata per review - Widen `normalize_metadata_value` / `build_dify_metadata` type hints to `Any` to match the str() coercion the implementations already do. - Reject only `None` and `""` in `build_dify_metadata`; other falsy values (e.g. numeric 0) flow through to normalization rather than being silently dropped, matching the design intent for the helper. - Merge into existing `target["metadata"]` instead of overwriting it when the caller has already populated the field; fall back to direct assignment if the existing value is not a dict. - Add tests covering the merge-with-existing path, the non-dict fallback, and the falsy-but-non-empty input case. Refs: langgenius/dify#35772, langgenius/dify-plugin-sdks#311 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…malize per review Per gemini-code-assist on langgenius#3233. Aligns 4 plugins on the same 'no side effects on existing args' principle. Refs: langgenius/dify#35772, langgenius/dify-plugin-sdks#311 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
E2E verified that the OpenAI API rejects `metadata` when store is false (BadRequestError: "The 'metadata' parameter is only allowed when 'store' is enabled."). The metadata feature therefore inherently requires store=true. apply_dify_metadata_if_enabled now sets store=true alongside the metadata (respecting an explicit store value already on the request), and the credential help text + inline comments document that requests/responses are persisted to Stored Completions on the OpenAI account. Refs: langgenius/dify#35772, langgenius/dify-plugin-sdks#311 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Adds opt-in support for attaching the Dify
app_idas OpenAI request metadata, so usage on the OpenAI Usage Dashboard can be filtered per Dify app.When the new
enable_request_metadatacredential is set toenabled, the plugin readsapp_idfrom the current Dify session (viaget_current_session()from the SDK) and attaches{dify_app_id, dify_source}as themetadatafield on bothchat.completions.createandresponses.create. The default isdisabled, so behavior is unchanged unless the operator opts in.This is the OpenAI counterpart of the same feature already shipped for Vertex AI (#3168) and Bedrock (#3201). The responsibility split mirrors those plugins:
generateContentrequestMetadataon Conversemetadataon Chat / Responsesstore=trueautomaticallyDesign note:
storeis set totruealongside metadataE2E verification against a live OpenAI account showed the API rejects
metadatawhenstoreis false:Enabling the metadata feature therefore inherently requires
store=true. The plugin makes this explicit:apply_dify_metadata_if_enabledsetsstore=truealongside the metadata (a non-destructive merge that respects an explicitstorevalue already on the request). This means requests and responses are persisted to Stored Completions on the OpenAI account when the operator opts in. The credential's help text inprovider/openai.yamldocuments this storage behavior in both English and Simplified Chinese, and links to the OpenAIstorereference.Bedrock (#3201) and Vertex AI (#3168) do not have this constraint, so their plugins remain unchanged.
Related:
Note
Draft / dependency.
pyproject.tomltemporarily pinsdify_pluginto the fork branchryuta-kobayashi-ug/dify-plugin-sdks@feat/pass-session-to-model-pluginsbecause it carries theget_current_session()API used here (langgenius/dify-plugin-sdks#313). The pin will revert to a versioneddify_pluginspec once that SDK change merges and a release ships.Change Type
Screenshots / Videos
store=trueand attachesdify_app_idanddify_source, so requests are recorded to Stored Completions and can be filtered per app.End-to-end screenshots will be added once the dependent SDK change (#313) ships and a Dify deployment can be exercised against a live OpenAI account with Stored Completions enabled.
LLM Plugin Checklist
Areas affected by this change
The change is additive and behind a credential that defaults to
disabled. No existing message flow, tool, multimodal, structured-output, or token-accounting code path is altered. Only_chat_generateand_build_responses_api_paramsgain a one-line opt-in hook that mutates the request kwargs immediately before the OpenAI client call.Version
versioninmanifest.yaml(0.4.1→0.4.3)dify_plugindeclared inpyproject.tomland locked inuv.lock(currently pinned to the SDK branch carrying fix getnumtoken retrun list[int] #313 — see Draft note above)Testing
uv run pytest tests/(22 passed): covers normalization (UUID passthrough, punctuation, mixed case, non-ASCII, 512-char truncation, empty string, non-string coercion),build_dify_metadata(None / empty / source marker / UUID passthrough / length normalization), andapply_dify_metadata_if_enabled(no-op on missing ordisabledcredential, silent on session-lookup failure, non-destructive metadata merge, and the newstore=truebehavior: setsstore=trueon opt-in, preserves an explicit caller-suppliedstore, and never touchesstorewhen disabled).