Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .changeset/anthropic-effort-param.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Add an opt-in to send the Anthropic output_config effort field for providers that require it. Set `effort_param` on the model config or `KIMI_MODEL_EFFORT_PARAM` to enable.
3 changes: 2 additions & 1 deletion docs/en/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ Each entry in the `models` table defines a model alias (the name used in `defaul
| `display_name` | `string` | No | Name shown in the UI; falls back to `model` when unset |
| `reasoning_key` | `string` | No | `openai` provider only. Override the field name used for reasoning content when the gateway returns it under a non-standard name; by default `reasoning_content`, `reasoning_details`, and `reasoning` are auto-detected |
| `adaptive_thinking` | `boolean` | No | `anthropic` provider only. Force adaptive thinking on or off, overriding the version inference based on the model name. Omit to infer automatically (Claude ≥ 4.6 uses adaptive) |
| `effort_param` | `boolean` | No | `anthropic` provider only. Opt in to sending `output_config.effort` on the enabled thinking path for endpoints that honor effort but whose model name is not recognized (e.g. third-party Anthropic-compatible gateways). Omit to send effort only for recognized model names |

When an alias contains `.`, use a quoted key:

Expand All @@ -184,7 +185,7 @@ max_context_size = 131072
display_name = "Kimi for Coding (custom)"
```

`[models."<alias>".overrides]` accepts ordinary model fields such as `max_context_size`, `max_input_size`, `max_output_size`, `capabilities`, `display_name`, `reasoning_key`, `adaptive_thinking`, `support_efforts`, `default_effort`, and `off_effort`. It does not accept identity / routing fields: `provider`, `model`, `protocol`, `beta_api`, and `base_url`.
`[models."<alias>".overrides]` accepts ordinary model fields such as `max_context_size`, `max_input_size`, `max_output_size`, `capabilities`, `display_name`, `reasoning_key`, `adaptive_thinking`, `effort_param`, `support_efforts`, `default_effort`, and `off_effort`. It does not accept identity / routing fields: `provider`, `model`, `protocol`, `beta_api`, and `base_url`.

You can also switch models temporarily without touching the config file — by setting `KIMI_MODEL_*` environment variables, the CLI synthesizes a temporary provider in memory that does not persist after restart. See [Define a model from environment variables](./env-vars.md#define-a-model-from-environment-variables-kimi-model).

Expand Down
1 change: 1 addition & 0 deletions docs/en/configuration/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ Complete variable list:
| `KIMI_MODEL_REASONING_KEY` | No | Reasoning field name override (`openai` only) | Auto-detected |
| `KIMI_MODEL_THINKING_EFFORT` | No | Thinking effort level: `low`/`medium`/`high`/`xhigh`/`max` | — |
| `KIMI_MODEL_ADAPTIVE_THINKING` | No | Force adaptive thinking on or off (`anthropic` only) | Inferred from model name |
| `KIMI_MODEL_EFFORT_PARAM` | No | Send the Anthropic `output_config.effort` field on the enabled thinking path even when the model name is not recognized (`anthropic` only) | Off |

If `KIMI_MODEL_NAME` is set but a required variable is missing, startup fails immediately with a clear error message.

Expand Down
3 changes: 2 additions & 1 deletion docs/zh/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ KIMI_BASE_URL = "https://api.moonshot.ai/v1"
| `display_name` | `string` | 否 | UI 中显示的名称,未设时回退到 `model` |
| `reasoning_key` | `string` | 否 | 仅 `openai` 供应商。当网关用非标准字段名返回推理内容时才需要设置;默认自动识别 `reasoning_content` / `reasoning_details` / `reasoning` |
| `adaptive_thinking` | `boolean` | 否 | 仅 `anthropic` 供应商。强制开启或关闭 adaptive thinking,覆盖按模型名推断的逻辑。省略时自动推断(Claude ≥ 4.6 使用 adaptive) |
| `effort_param` | `boolean` | 否 | 仅 `anthropic` 供应商。选择性地在 enabled thinking 路径上发送 `output_config.effort` 字段,适用于模型名无法被识别、但支持 effort 的端点(例如第三方 Anthropic 兼容网关)。省略时仅对可识别的模型名发送 effort |

别名中含 `.` 时需要加引号:

Expand All @@ -184,7 +185,7 @@ max_context_size = 131072
display_name = "Kimi for Coding (custom)"
```

`[models."<alias>".overrides]` 接受普通模型字段,例如 `max_context_size`、`max_input_size`、`max_output_size`、`capabilities`、`display_name`、`reasoning_key`、`adaptive_thinking`、`support_efforts`、`default_effort` 和 `off_effort`。不接受身份 / 路由字段:`provider`、`model`、`protocol`、`beta_api` 和 `base_url`。
`[models."<alias>".overrides]` 接受普通模型字段,例如 `max_context_size`、`max_input_size`、`max_output_size`、`capabilities`、`display_name`、`reasoning_key`、`adaptive_thinking`、`effort_param`、`support_efforts`、`default_effort` 和 `off_effort`。不接受身份 / 路由字段:`provider`、`model`、`protocol`、`beta_api` 和 `base_url`。

无需修改配置文件也可以临时切换模型——通过 `KIMI_MODEL_*` 环境变量在内存里合成一个临时供应商,详见[用环境变量定义模型](./env-vars.md#用环境变量定义模型-kimi-model)。

Expand Down
1 change: 1 addition & 0 deletions docs/zh/configuration/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ kimi
| `KIMI_MODEL_REASONING_KEY` | 否 | 推理字段名覆盖(仅 `openai`) | 自动探测 |
| `KIMI_MODEL_THINKING_EFFORT` | 否 | Thinking 强度:`low`/`medium`/`high`/`xhigh`/`max` | — |
| `KIMI_MODEL_ADAPTIVE_THINKING` | 否 | 强制开启或关闭 adaptive thinking(仅 `anthropic`) | 按模型名推断 |
| `KIMI_MODEL_EFFORT_PARAM` | 否 | 在 enabled thinking 路径上发送 Anthropic `output_config.effort` 字段,即使模型名无法被识别(仅 `anthropic`) | 关闭 |

设置了 `KIMI_MODEL_NAME` 但缺少必填变量时,启动会立即失败并给出明确提示。

Expand Down
3 changes: 3 additions & 0 deletions packages/agent-core-v2/docs/config-manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ merge_all_available_skills = true
# display_name: string
# reasoning_key: string
# adaptive_thinking: boolean
# effort_param: boolean
# beta_api: boolean
# support_efforts: string[]
# default_effort: string
Expand All @@ -239,6 +240,7 @@ merge_all_available_skills = true
# display_name: string
# reasoning_key: string
# adaptive_thinking: boolean
# effort_param: boolean
# support_efforts: string[]
# default_effort: string
# off_effort: string
Expand Down Expand Up @@ -303,6 +305,7 @@ merge_all_available_skills = true
# display_name: string
# reasoning_key: string
# adaptive_thinking: boolean
# effort_param: boolean
# support_efforts: string[]
# default_effort: string
# off_effort: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ const ModelBaseSchema = z.object({
displayName: z.string().optional(),
reasoningKey: z.string().optional(),
adaptiveThinking: z.boolean().optional(),
effortParam: z.boolean().optional(),
betaApi: z.boolean().optional(),
supportEfforts: z.array(z.string()).optional(),
defaultEffort: z.string().optional(),
Expand Down
5 changes: 5 additions & 0 deletions packages/agent-core-v2/src/app/kosongConfig/envOverlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ export const kimiModelEnvOverlay: ConfigEffectiveOverlay = {
getEnv('KIMI_MODEL_ADAPTIVE_THINKING'),
'KIMI_MODEL_ADAPTIVE_THINKING',
);
const effortParam = parseBooleanVar(
getEnv('KIMI_MODEL_EFFORT_PARAM'),
'KIMI_MODEL_EFFORT_PARAM',
);

const alias: Record<string, unknown> = {
provider: ENV_MODEL_PROVIDER_KEY,
Expand All @@ -161,6 +165,7 @@ export const kimiModelEnvOverlay: ConfigEffectiveOverlay = {
if (maxOutputSize !== undefined) alias['maxOutputSize'] = maxOutputSize;
if (reasoningKey !== undefined) alias['reasoningKey'] = reasoningKey;
if (adaptiveThinking !== undefined) alias['adaptiveThinking'] = adaptiveThinking;
if (effortParam !== undefined) alias['effortParam'] = effortParam;

const models = asRecord(effective['models']);
const nextModels = { ...models, [ENV_MODEL_ALIAS_KEY]: alias };
Expand Down
1 change: 1 addition & 0 deletions packages/agent-core-v2/src/kosong/model/catalogService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,7 @@ function buildProtocolProviderOptions(
if (model.maxOutputSize !== undefined) options.defaultMaxTokens = model.maxOutputSize;
if (model.supportEfforts !== undefined) options.supportEfforts = model.supportEfforts;
if (model.adaptiveThinking !== undefined) options.adaptiveThinking = model.adaptiveThinking;
if (model.effortParam !== undefined) options.effortParam = model.effortParam;
if (model.betaApi !== undefined) options.betaApi = model.betaApi;
break;
case 'openai': {
Expand Down
1 change: 1 addition & 0 deletions packages/agent-core-v2/src/kosong/model/inspection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ const PROVIDER_OPTION_FIELD: Readonly<Record<string, string>> = {
defaultMaxTokens: 'maxOutputSize',
supportEfforts: 'supportEfforts',
adaptiveThinking: 'adaptiveThinking',
effortParam: 'effortParam',
betaApi: 'betaApi',
reasoningKey: 'reasoningKey',
};
Expand Down
2 changes: 2 additions & 0 deletions packages/agent-core-v2/src/kosong/model/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface ModelOverride {
displayName?: string;
reasoningKey?: string;
adaptiveThinking?: boolean;
effortParam?: boolean;
supportEfforts?: string[];
defaultEffort?: string;
offEffort?: string;
Expand All @@ -67,6 +68,7 @@ export interface ModelRecord {
displayName?: string;
reasoningKey?: string;
adaptiveThinking?: boolean;
effortParam?: boolean;
betaApi?: boolean;
supportEfforts?: string[];
defaultEffort?: string;
Expand Down
1 change: 1 addition & 0 deletions packages/agent-core-v2/src/kosong/protocol/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export interface ProtocolProviderOptions {
readonly supportEfforts?: readonly string[];
readonly offEffort?: string;
readonly adaptiveThinking?: boolean;
readonly effortParam?: boolean;
readonly betaApi?: boolean;
readonly metadata?: Readonly<Record<string, string>>;
readonly vertexai?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ registerProtocolBase({
defaultMaxTokens: config.providerOptions?.defaultMaxTokens,
adaptiveThinking: config.providerOptions?.adaptiveThinking,
supportEfforts: config.providerOptions?.supportEfforts,
effortParam: config.providerOptions?.effortParam,
betaApi: config.providerOptions?.betaApi,
metadata:
config.providerOptions?.metadata === undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
* headers vs the beta endpoint, and the thinking profile matrix (budget vs
* adaptive).
*
* `output_config.effort` is emitted only for name-recognized (adaptive)
* profiles; the `effortParam` option is an opt-in for third-party
* Anthropic-compatible endpoints (e.g. z.ai GLM over `/api/anthropic`) that
* honor effort on the enabled thinking path without a recognizable model
* name. budget_tokens is still sent there (required by Anthropic's
* enabled-thinking contract; ignored by such gateways).
*
* The hook surface is `withThinking` plus `convertError`. `withThinking`
* lets a vendor dialect running over this transport re-encode the thinking
* intent; when the per-turn thinking intent carries `keep`, the BASE
Expand Down Expand Up @@ -144,6 +151,7 @@ export interface AnthropicOptions {
stream?: boolean | undefined;
adaptiveThinking?: boolean | undefined;
supportEfforts?: readonly string[] | undefined;
effortParam?: boolean;
betaApi?: boolean | undefined;
thinkingEffort?: ThinkingEffort | undefined;
clientFactory?: (auth: ProviderRequestAuth) => Anthropic;
Expand Down Expand Up @@ -811,6 +819,7 @@ export class AnthropicChatProvider implements ChatProvider {
private readonly _clientFactory: ((auth: ProviderRequestAuth) => Anthropic) | undefined;
private readonly _adaptiveThinking: boolean | undefined;
private readonly _supportEfforts: readonly string[] | undefined;
private readonly _effortParam: boolean | undefined;
private readonly _betaApi: boolean;
private readonly _thinkingEffort: ThinkingEffort | undefined;
private readonly _explicitMaxTokens: boolean;
Expand All @@ -822,6 +831,7 @@ export class AnthropicChatProvider implements ChatProvider {
this._metadata = options.metadata;
this._adaptiveThinking = options.adaptiveThinking;
this._supportEfforts = options.supportEfforts;
this._effortParam = options.effortParam;
this._betaApi = options.betaApi ?? false;
this._thinkingEffort = options.thinkingEffort;
this._hooks = options.hooks;
Expand Down Expand Up @@ -1089,7 +1099,10 @@ export class AnthropicChatProvider implements ChatProvider {
: { type: 'enabled', budget_tokens: budgetTokens },
betaFeatures: newBetas,
};
if ((profile.supportsEffortParam || budgetTokens === undefined) && effort !== 'on') {
if (
(profile.supportsEffortParam || this._effortParam === true || budgetTokens === undefined) &&
effort !== 'on'
) {
patch.output_config = { effort } as MessageCreateParams['output_config'];
} else {
patch.output_config = undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ describe('kimiModelEnvOverlay.apply', () => {
KIMI_MODEL_DISPLAY_NAME: 'Mine',
KIMI_MODEL_REASONING_KEY: 'reasoning_content',
KIMI_MODEL_ADAPTIVE_THINKING: 'true',
KIMI_MODEL_EFFORT_PARAM: 'true',
});
expect((effective['models'] as Record<string, unknown>)[ENV_MODEL_ALIAS_KEY]).toEqual({
provider: ENV_MODEL_PROVIDER_KEY,
Expand All @@ -113,6 +114,7 @@ describe('kimiModelEnvOverlay.apply', () => {
displayName: 'Mine',
reasoningKey: 'reasoning_content',
adaptiveThinking: true,
effortParam: true,
});

expect(() => apply({}, { KIMI_MODEL_NAME: 'm', KIMI_MODEL_MAX_CONTEXT_SIZE: 'abc' })).toThrowError(
Expand Down
3 changes: 3 additions & 0 deletions packages/agent-core-v2/test/app/model/model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ describe('kimiModelEnvOverlay', () => {
KIMI_MODEL_DISPLAY_NAME: 'Custom Model',
KIMI_MODEL_REASONING_KEY: 'reasoning',
KIMI_MODEL_ADAPTIVE_THINKING: 'true',
KIMI_MODEL_EFFORT_PARAM: 'true',
KIMI_MODEL_TEMPERATURE: '0.3',
KIMI_MODEL_TOP_P: ' 0.95 ',
KIMI_MODEL_THINKING_KEEP: 'all',
Expand All @@ -465,6 +466,7 @@ describe('kimiModelEnvOverlay', () => {
displayName: 'Custom Model',
reasoningKey: 'reasoning',
adaptiveThinking: true,
effortParam: true,
});
expect(effective['modelOverrides']).toEqual({
temperature: 0.3,
Expand All @@ -488,6 +490,7 @@ describe('kimiModelEnvOverlay', () => {
['KIMI_MODEL_MAX_CONTEXT_SIZE', '1.5'],
['KIMI_MODEL_MAX_OUTPUT_SIZE', 'nope'],
['KIMI_MODEL_ADAPTIVE_THINKING', 'maybe'],
['KIMI_MODEL_EFFORT_PARAM', 'maybe'],
['KIMI_MODEL_TEMPERATURE', 'abc'],
['KIMI_MODEL_TEMPERATURE', '1.2.3'],
['KIMI_MODEL_TOP_P', 'NaN'],
Expand Down
22 changes: 22 additions & 0 deletions packages/agent-core-v2/test/kosong/model/catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,28 @@ describe('Model assembly (pure data)', () => {
}
});

it('passes a declared effortParam through providerOptions for the Anthropic wire', () => {
const { host, catalog } = createHost({
providers: {
zai: {
type: 'anthropic',
apiKey: 'sk-z',
baseUrl: 'https://zai.example.test/api/anthropic',
},
},
models: {
glm: { provider: 'zai', model: 'glm-5.2', maxContextSize: 200000, effortParam: true },
plain: { provider: 'zai', model: 'glm-4.6', maxContextSize: 200000 },
},
});
try {
expect(catalog.get('glm').providerOptions).toEqual({ effortParam: true });
expect(catalog.get('plain').providerOptions).toBeUndefined();
} finally {
host.dispose();
}
});

it('enables google-genai vertex mode through providerOptions when project and location resolve', () => {
const { host, catalog } = createHost({
providers: {
Expand Down
3 changes: 3 additions & 0 deletions packages/agent-core-v2/test/kosong/model/modelService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe('models TOML transforms', () => {
display_name: 'K2',
reasoning_key: 'reasoning_content',
adaptive_thinking: true,
effort_param: true,
beta_api: true,
support_efforts: ['low', 'high'],
default_effort: 'high',
Expand All @@ -40,6 +41,7 @@ describe('models TOML transforms', () => {
displayName: 'K2',
reasoningKey: 'reasoning_content',
adaptiveThinking: true,
effortParam: true,
betaApi: true,
supportEfforts: ['low', 'high'],
defaultEffort: 'high',
Expand All @@ -55,6 +57,7 @@ describe('models TOML transforms', () => {
display_name: 'K2',
reasoning_key: 'reasoning_content',
adaptive_thinking: true,
effort_param: true,
beta_api: true,
support_efforts: ['low', 'high'],
default_effort: 'high',
Expand Down
31 changes: 31 additions & 0 deletions packages/agent-core-v2/test/kosong/provider/composition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,37 @@ describe('per-turn intent wire encoding (behavior probes)', () => {
// adds nothing else: no beta header reaches the wire at all.
expect(requestOptions).toBeUndefined();
});

it('emits output_config.effort on the budget path only with the effortParam opt-in', async () => {
// Third-party Anthropic-compatible endpoints (e.g. z.ai GLM over
// /api/anthropic) honor output_config.effort on the enabled thinking path
// even though their model name is not name-recognized.
// adaptiveThinking:false forces budget mode (supportsEffortParam:false),
// so effortParam is the ONLY way effort reaches the wire for these models.
const optedIn = new AnthropicChatProvider({
model: 'glm-5.2',
apiKey: 'sk-probe',
stream: false,
adaptiveThinking: false,
effortParam: true,
});
const { params } = await captureAnthropicBody(optedIn, { thinking: { effort: 'high' } });
expect(params['thinking']).toEqual({ type: 'enabled', budget_tokens: 32000 });
expect(params['output_config']).toEqual({ effort: 'high' });

// Same budget-mode config, no opt-in -> budget_tokens only.
const optedOut = new AnthropicChatProvider({
model: 'glm-5.2',
apiKey: 'sk-probe',
stream: false,
adaptiveThinking: false,
});
const { params: plainParams } = await captureAnthropicBody(optedOut, {
thinking: { effort: 'high' },
});
expect(plainParams['thinking']).toEqual({ type: 'enabled', budget_tokens: 32000 });
expect(plainParams['output_config']).toBeUndefined();
});
});

describe('quota-exhausted classification through the real composition (behavior probes)', () => {
Expand Down
5 changes: 5 additions & 0 deletions packages/agent-core/src/config/env-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ export function applyEnvModelConfig(config: KimiConfig, env: Env = process.env):
env['KIMI_MODEL_ADAPTIVE_THINKING'],
'KIMI_MODEL_ADAPTIVE_THINKING',
);
const effortParam = parseBooleanVar(
env['KIMI_MODEL_EFFORT_PARAM'],
'KIMI_MODEL_EFFORT_PARAM',
);

const alias: ModelAlias = {
provider: ENV_MODEL_PROVIDER_KEY,
Expand All @@ -136,6 +140,7 @@ export function applyEnvModelConfig(config: KimiConfig, env: Env = process.env):
...(maxOutputSize !== undefined ? { maxOutputSize } : {}),
...(reasoningKey !== undefined ? { reasoningKey } : {}),
...(adaptiveThinking !== undefined ? { adaptiveThinking } : {}),
effortParam,
};

const thinkingEffort = trimmed(env['KIMI_MODEL_THINKING_EFFORT']);
Expand Down
8 changes: 8 additions & 0 deletions packages/agent-core/src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ const ModelAliasBaseSchema = z.object({
// model-name version inference. Needed for custom-named Anthropic endpoints
// whose model name does not encode a parseable Claude version.
adaptiveThinking: z.boolean().optional(),
// Opt-in: declare that this model's endpoint honors Anthropic
// `output_config.effort` on the *enabled* thinking path
// (`thinking: { type: 'enabled' }`). kosong only emits `output_config.effort`
// for model names it recognizes (opus-4-5 / adaptive); third-party
// Anthropic-compatible endpoints (e.g. z.ai GLM over /api/anthropic) honor
// effort but are not name-recognized, so set this to make `/thinking`
// effort levels actually take effect on them.
effortParam: z.boolean().optional(),
// Efforts (e.g. ["low", "high", "max"]) the model supports for
// extended thinking, plus the catalog default. Generic to any provider:
// managed models fill these from the catalog, others can be set by hand in
Expand Down
3 changes: 3 additions & 0 deletions packages/agent-core/src/session/provider-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export class ProviderManager implements ModelProvider {
effectiveAlias.offEffort,
effectiveAlias.adaptiveThinking,
alias.betaApi,
effectiveAlias.effortParam,
);

return {
Expand Down Expand Up @@ -270,6 +271,7 @@ function toKosongProviderConfig(
offEffort: string | undefined,
adaptiveThinking: boolean | undefined,
betaApi: boolean | undefined,
effortParam: boolean | undefined,
): KosongProviderConfig {
const effectiveType = modelProtocol === 'anthropic' ? 'anthropic' : provider.type;
const envCustomHeaders = parseKimiCodeCustomHeaders();
Expand All @@ -296,6 +298,7 @@ function toKosongProviderConfig(
...(provider.type === 'kimi'
? { kimiThinking: true, convertError: classifyKimiQuotaError }
: {}),
effortParam,
...(betaApi !== undefined ? { betaApi } : {}),
// Session affinity: Anthropic's analog of OpenAI `prompt_cache_key` is
// `metadata.user_id` on the Messages API (cache-affinity / end-user id).
Expand Down
Loading