@truefoundry/assistant-ui-runtime 0.1.17 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { AgentSessionClient } from 'truefoundry-gateway-sdk/agents';
2
2
  import { PrivateAgentSessionClient } from 'truefoundry-gateway-sdk/agents/private';
3
- import { A as AgentSpec, N as CreateSessionRequest, Y as ListSessionsParams, S as Session, d as Turn, aE as TurnState, a as AgentChatServer, aM as UpdateSessionRequest, k as AgentSelectorEntry, G as ConnectorSelectorEntry, a8 as ModelSelectorEntry, az as SkillSelectorEntry, g as AgentBuilderServer } from '../../types-CctJojlz.js';
3
+ import { A as AgentSpec, N as CreateSessionRequest, Y as ListSessionsParams, S as Session, d as Turn, aE as TurnState, a as AgentChatServer, aM as UpdateSessionRequest, k as AgentSelectorEntry, G as ConnectorSelectorEntry, a8 as ModelSelectorEntry, az as SkillSelectorEntry, g as AgentBuilderServer } from '../../types-BRrQpoVy.js';
4
4
  import { TruefoundryGatewayApi } from 'truefoundry-gateway-sdk';
5
5
 
6
6
  /**
@@ -116,7 +116,11 @@ declare function createTrueFoundryChatServer<TSpec extends TfyAgentSpec = TfyAge
116
116
  */
117
117
 
118
118
  interface TfyModelSelectorEntry extends ModelSelectorEntry {
119
- /** Write into AgentSpec.model.name (model_fqn). */
119
+ /**
120
+ * model_fqn for AgentSpec.model.name.
121
+ * Same value as {@link ModelSelectorEntry.name} / `id` — trueforge-ui writes
122
+ * `name` into the spec; keep this alias for hosts that still read `apiModel`.
123
+ */
120
124
  apiModel: string;
121
125
  modelId: string;
122
126
  providerAccount?: string;
@@ -7,7 +7,7 @@ import {
7
7
  isTfyMcpToolInfo,
8
8
  isTfySystemToolInfo,
9
9
  isTfyToolInfo
10
- } from "../../chunk-S4SH2KT2.js";
10
+ } from "../../chunk-N6LJ3LR2.js";
11
11
  export {
12
12
  createTrueFoundryAgentUIServer,
13
13
  createTrueFoundryChatServer,
@@ -1,5 +1,5 @@
1
- import { aH as TurnStreamingEvent, aO as DeltaEvents, T as TurnEvent } from '../types-CctJojlz.js';
2
- export { aP as ActionRequiredEvent, f as AgentBuilderCapabilitiesResponse, g as AgentBuilderServer, h as AgentCapabilityConfig, a as AgentChatServer, aQ as AgentInfo, i as AgentLibraryEntry, aR as AgentParent, j as AgentRuntimeConfig, k as AgentSelectorEntry, l as AgentSkill, A as AgentSpec, m as AgentUIServer, n as AgentUIServerPort, o as ApprovalDecision, p as AuthenticateConnectorRequest, C as CatalogServer, aS as ChunkDeltaToolCall, q as ConnectorAuth, r as ConnectorAuthApiKey, s as ConnectorAuthNone, t as ConnectorAuthOAuth, u as ConnectorAuthPublic, v as ConnectorAuthPublicApiKey, w as ConnectorAuthPublicNone, x as ConnectorAuthPublicOAuth, y as ConnectorAuthType, z as ConnectorAuthenticationResult, B as ConnectorBase, D as ConnectorCatalogEntry, E as ConnectorCatalogServer, F as ConnectorConfigBase, G as ConnectorSelectorEntry, H as ConnectorState, I as CreateConnectorRequest, J as CreateModelProviderRequest, K as CreateSandboxProviderRequest, L as CreateSandboxRequest, N as CreateSessionRequest, O as CreateSkillRequest, P as CreateSkillRequestBase, Q as DefinedSkill, R as GithubSkill, V as ImportGithubSkillRequest, W as ListResult, X as ListSessionsOrder, Y as ListSessionsParams, M as McpAuthRequiredEvent, aT as McpInitializeEvent, aU as McpServerAuthInfo, Z as McpServerMount, _ as Model, $ as ModelCatalogServer, a0 as ModelEntry, aV as ModelMessageContentPart, aW as ModelMessageDeltaEvent, a1 as ModelMessageEvent, a2 as ModelParams, a3 as ModelProperties, a4 as ModelProviderBase, a5 as ModelProviderCatalogEntry, a6 as ModelProviderConfigBase, a7 as ModelSelection, a8 as ModelSelectorEntry, a9 as PageParams, aa as PreviousTurnIdInput, ab as ProviderEntry, ac as ProviderType, ad as RegistrySkill, ae as SandboxBase, af as SandboxCatalogEntry, ag as SandboxCatalogServer, ah as SandboxConfig, ai as SandboxCreatedEvent, aj as SandboxProviderBase, ak as SandboxProviderCatalogEntry, al as SandboxProviderConfig, am as SandboxProviderListEntry, an as SandboxSnapshotSyncStatus, ao as SaveAgentRequest, ap as SaveAgentResult, aq as SearchAgentSelectorParams, ar as SearchAgentsParams, as as SelectRegistrySkillRequest, S as Session, at as SessionEventItem, au as SkillBase, av as SkillCatalogEntry, aw as SkillCatalogServer, ax as SkillConfigBase, ay as SkillMount, az as SkillSelectorEntry, b as ThreadCreatedEvent, aX as ThreadDoneEvent, aA as ToolApprovalRequiredEvent, aB as ToolBase, aC as ToolCall, aY as ToolCallFunction, aZ as ToolCallRef, a_ as ToolInfo, a$ as ToolResponseEvent, aD as ToolResponseRequiredEvent, d as Turn, b0 as TurnCreatedEvent, b1 as TurnDoneEvent, e as TurnInputItem, aE as TurnState, b2 as TurnStateCancelled, aF as TurnStateDone, b3 as TurnStateError, b4 as TurnStateRunning, aG as TurnStreamData, aI as UpdateConnectorRequest, aJ as UpdateModelProviderRequest, aK as UpdateSandboxProviderRequest, aL as UpdateSandboxRequest, aM as UpdateSessionRequest, aN as UserMessage, b5 as UserMessageContent, c as UserToolApprovalEvent, U as UserToolResponseEvent } from '../types-CctJojlz.js';
1
+ import { aH as TurnStreamingEvent, aO as DeltaEvents, T as TurnEvent } from '../types-BRrQpoVy.js';
2
+ export { aP as ActionRequiredEvent, f as AgentBuilderCapabilitiesResponse, g as AgentBuilderServer, h as AgentCapabilityConfig, a as AgentChatServer, aQ as AgentInfo, i as AgentLibraryEntry, aR as AgentParent, j as AgentRuntimeConfig, k as AgentSelectorEntry, l as AgentSkill, A as AgentSpec, m as AgentUIServer, n as AgentUIServerPort, o as ApprovalDecision, p as AuthenticateConnectorRequest, C as CatalogServer, aS as ChunkDeltaToolCall, q as ConnectorAuth, r as ConnectorAuthApiKey, s as ConnectorAuthNone, t as ConnectorAuthOAuth, u as ConnectorAuthPublic, v as ConnectorAuthPublicApiKey, w as ConnectorAuthPublicNone, x as ConnectorAuthPublicOAuth, y as ConnectorAuthType, z as ConnectorAuthenticationResult, B as ConnectorBase, D as ConnectorCatalogEntry, E as ConnectorCatalogServer, F as ConnectorConfigBase, G as ConnectorSelectorEntry, H as ConnectorState, I as CreateConnectorRequest, J as CreateModelProviderRequest, K as CreateSandboxProviderRequest, L as CreateSandboxRequest, N as CreateSessionRequest, O as CreateSkillRequest, P as CreateSkillRequestBase, Q as DefinedSkill, R as GithubSkill, V as ImportGithubSkillRequest, W as ListResult, X as ListSessionsOrder, Y as ListSessionsParams, M as McpAuthRequiredEvent, aT as McpInitializeEvent, aU as McpServerAuthInfo, Z as McpServerMount, _ as Model, $ as ModelCatalogServer, a0 as ModelEntry, aV as ModelMessageContentPart, aW as ModelMessageDeltaEvent, a1 as ModelMessageEvent, a2 as ModelParams, a3 as ModelProperties, a4 as ModelProviderBase, a5 as ModelProviderCatalogEntry, a6 as ModelProviderConfigBase, a7 as ModelSelection, a8 as ModelSelectorEntry, a9 as PageParams, aa as PreviousTurnIdInput, ab as ProviderEntry, ac as ProviderType, ad as RegistrySkill, ae as SandboxBase, af as SandboxCatalogEntry, ag as SandboxCatalogServer, ah as SandboxConfig, ai as SandboxCreatedEvent, aj as SandboxProviderBase, ak as SandboxProviderCatalogEntry, al as SandboxProviderConfig, am as SandboxProviderListEntry, an as SandboxSnapshotSyncStatus, ao as SaveAgentRequest, ap as SaveAgentResult, aq as SearchAgentSelectorParams, ar as SearchAgentsParams, as as SelectRegistrySkillRequest, S as Session, at as SessionEventItem, au as SkillBase, av as SkillCatalogEntry, aw as SkillCatalogServer, ax as SkillConfigBase, ay as SkillMount, az as SkillSelectorEntry, b as ThreadCreatedEvent, aX as ThreadDoneEvent, aA as ToolApprovalRequiredEvent, aB as ToolBase, aC as ToolCall, aY as ToolCallFunction, aZ as ToolCallRef, a_ as ToolInfo, a$ as ToolResponseEvent, aD as ToolResponseRequiredEvent, d as Turn, b0 as TurnCreatedEvent, b1 as TurnDoneEvent, e as TurnInputItem, aE as TurnState, b2 as TurnStateCancelled, aF as TurnStateDone, b3 as TurnStateError, b4 as TurnStateRunning, aG as TurnStreamData, aI as UpdateConnectorRequest, aJ as UpdateModelProviderRequest, aK as UpdateSandboxProviderRequest, aL as UpdateSandboxRequest, aM as UpdateSessionRequest, aN as UserMessage, b5 as UserMessageContent, c as UserToolApprovalEvent, U as UserToolResponseEvent } from '../types-BRrQpoVy.js';
3
3
 
4
4
  /**
5
5
  * Local implementations of streaming delta helpers.
@@ -632,6 +632,7 @@ type ConnectorAuthPublic = ConnectorAuthPublicOAuth | ConnectorAuthPublicApiKey
632
632
  */
633
633
  interface ConnectorConfigBase<TAuth extends ConnectorAuth = ConnectorAuth> {
634
634
  name: string;
635
+ description: string;
635
636
  url: string;
636
637
  auth: TAuth;
637
638
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truefoundry/assistant-ui-runtime",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "TrueFoundry Gateway agent runtime adapter for assistant-ui",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -81,4 +81,4 @@
81
81
  "typescript": "^5.9.3",
82
82
  "vitest": "4.1.9"
83
83
  }
84
- }
84
+ }
package/src/hooks.ts CHANGED
@@ -6,7 +6,8 @@ import { useAui, useAuiState } from "@assistant-ui/store";
6
6
  import type { TrueFoundryMessageCustomMetadata } from "./messageCustomMetadata.js";
7
7
  import {
8
8
  EMPTY_DRAFT_EXTRAS,
9
- trueFoundryExtras,
9
+ getTrueFoundryExtras,
10
+ useTrueFoundryRuntimeExtras,
10
11
  type TrueFoundryDraftRuntimeExtras,
11
12
  } from "./truefoundryExtras.js";
12
13
  import type { RespondToToolApprovalOptions } from "./toolApproval.js";
@@ -14,7 +15,7 @@ import type { RespondToToolResponseOptions } from "./toolResponse.js";
14
15
 
15
16
  /** Pending tool approvals plus a respond action. */
16
17
  export const useTrueFoundryApprovals = () => {
17
- const extras = trueFoundryExtras.use((e) => e, undefined);
18
+ const extras = useTrueFoundryRuntimeExtras();
18
19
 
19
20
  return useMemo(
20
21
  () => ({
@@ -31,7 +32,7 @@ export const useTrueFoundryApprovals = () => {
31
32
 
32
33
  /** Pending ask-user tool responses plus a respond action. */
33
34
  export const useTrueFoundryToolResponses = () => {
34
- const extras = trueFoundryExtras.use((e) => e, undefined);
35
+ const extras = useTrueFoundryRuntimeExtras();
35
36
 
36
37
  return useMemo(
37
38
  () => ({
@@ -48,7 +49,7 @@ export const useTrueFoundryToolResponses = () => {
48
49
 
49
50
  /** Pending MCP OAuth plus a resume action. */
50
51
  export const useTrueFoundryMcpAuth = () => {
51
- const extras = trueFoundryExtras.use((e) => e, undefined);
52
+ const extras = useTrueFoundryRuntimeExtras();
52
53
 
53
54
  return useMemo(
54
55
  () => ({
@@ -67,25 +68,25 @@ export const useTrueFoundryMcpAuth = () => {
67
68
  export const useTrueFoundryRespondToToolApproval = () => {
68
69
  const aui = useAui();
69
70
  return (response: RespondToToolApprovalOptions) =>
70
- trueFoundryExtras.get(aui).respondToToolApproval(response);
71
+ getTrueFoundryExtras(aui).respondToToolApproval(response);
71
72
  };
72
73
 
73
74
  /** Returns a function to respond to a pending tool response from any render context. */
74
75
  export const useTrueFoundryRespondToToolResponse = () => {
75
76
  const aui = useAui();
76
77
  return (response: RespondToToolResponseOptions) =>
77
- trueFoundryExtras.get(aui).respondToToolResponse(response);
78
+ getTrueFoundryExtras(aui).respondToToolResponse(response);
78
79
  };
79
80
 
80
81
  /** Returns a function to resume after MCP OAuth from any render context. */
81
82
  export const useTrueFoundryResumeMcpAuth = () => {
82
83
  const aui = useAui();
83
- return () => trueFoundryExtras.get(aui).resumeMcpAuth();
84
+ return () => getTrueFoundryExtras(aui).resumeMcpAuth();
84
85
  };
85
86
 
86
87
  /** Current sandboxId for this session, if a sandbox has been created. */
87
88
  export const useTrueFoundrySandboxId = (): string | undefined =>
88
- trueFoundryExtras.use((e) => e.sandboxId, undefined);
89
+ useTrueFoundryRuntimeExtras()?.sandboxId;
89
90
 
90
91
  /** Turn that produced the message being rendered. Only defined inside a message scope. */
91
92
  export const useTrueFoundryTurnId = (): string | undefined =>
@@ -108,25 +109,25 @@ export const useTrueFoundryDownloadSandboxFile = () => {
108
109
  "Downloading a sandbox file requires a message scope to resolve its turn.",
109
110
  );
110
111
  }
111
- return trueFoundryExtras.get(aui).downloadSandboxFile({ turnId, path });
112
+ return getTrueFoundryExtras(aui).downloadSandboxFile({ turnId, path });
112
113
  };
113
114
  };
114
115
 
115
116
  /** Returns a function to cancel the current run from any render context. */
116
117
  export const useTrueFoundryCancel = () => {
117
118
  const aui = useAui();
118
- return () => trueFoundryExtras.get(aui).cancel();
119
+ return () => getTrueFoundryExtras(aui).cancel();
119
120
  };
120
121
 
121
122
  /** Returns a function to reload (retry) the current session from any render context. */
122
123
  export const useTrueFoundryReload = () => {
123
124
  const aui = useAui();
124
- return () => trueFoundryExtras.get(aui).reload();
125
+ return () => getTrueFoundryExtras(aui).reload();
125
126
  };
126
127
 
127
128
  /** Older history pagination state plus a load-more action for scroll-up. */
128
129
  export const useTrueFoundryHistoryPagination = () => {
129
- const extras = trueFoundryExtras.use((e) => e, undefined);
130
+ const extras = useTrueFoundryRuntimeExtras();
130
131
 
131
132
  return useMemo(
132
133
  () => ({
@@ -147,17 +148,17 @@ export const useTrueFoundryHistoryPagination = () => {
147
148
  * arrive in this client. Falls back to `false` on runtimes that predate the flag.
148
149
  */
149
150
  export const useTrueFoundryResumeUnavailable = () =>
150
- trueFoundryExtras.use((e) => e.resumeUnavailable, false) ?? false;
151
+ useTrueFoundryRuntimeExtras()?.resumeUnavailable ?? false;
151
152
 
152
153
  /** Returns a function to reset (re-submit) a user turn from any render context. */
153
154
  export const useTrueFoundryResetFromTurn = () => {
154
155
  const aui = useAui();
155
- return (turnId: string) => trueFoundryExtras.get(aui).resetFromTurn(turnId);
156
+ return (turnId: string) => getTrueFoundryExtras(aui).resetFromTurn(turnId);
156
157
  };
157
158
 
158
159
  /** Current draft agent spec and sync state (draft mode only). */
159
160
  export const useTrueFoundryAgentSpec = () => {
160
- const extras = trueFoundryExtras.use((e) => e.draft, null);
161
+ const extras = useTrueFoundryRuntimeExtras()?.draft ?? null;
161
162
 
162
163
  return useMemo(
163
164
  () => ({ ...EMPTY_DRAFT_EXTRAS, ...extras }),
@@ -169,14 +170,14 @@ export const useTrueFoundryAgentSpec = () => {
169
170
  export const useTrueFoundryUpdateAgentSpec = () => {
170
171
  const aui = useAui();
171
172
  return (update: Parameters<TrueFoundryDraftRuntimeExtras["updateAgentSpec"]>[0]) =>
172
- trueFoundryExtras.get(aui).draft?.updateAgentSpec(update);
173
+ getTrueFoundryExtras(aui).draft?.updateAgentSpec(update);
173
174
  };
174
175
 
175
176
  /** Flushes any pending draft-spec synchronization before a coordinated write. */
176
177
  export const useTrueFoundryFlushAgentSpec = () => {
177
178
  const aui = useAui();
178
179
  return () =>
179
- trueFoundryExtras.get(aui).draft?.flushAgentSpec() ?? Promise.resolve();
180
+ getTrueFoundryExtras(aui).draft?.flushAgentSpec() ?? Promise.resolve();
180
181
  };
181
182
 
182
183
  /** Adopts a spec already persisted by another server operation without syncing again. */
@@ -184,6 +185,5 @@ export const useTrueFoundryAdoptAgentSpec = () => {
184
185
  const aui = useAui();
185
186
  return (
186
187
  request: Parameters<TrueFoundryDraftRuntimeExtras["adoptAgentSpec"]>[0],
187
- ) => trueFoundryExtras.get(aui).draft?.adoptAgentSpec(request);
188
+ ) => getTrueFoundryExtras(aui).draft?.adoptAgentSpec(request);
188
189
  };
189
-
package/src/index.ts CHANGED
@@ -42,7 +42,11 @@ export type {
42
42
  } from "./messageCustomMetadata.js";
43
43
  export type { PendingApproval, PendingToolResponse } from "./collectPending.js";
44
44
  export type { TrueFoundryRuntimeExtras } from "./truefoundryExtras.js";
45
- export { trueFoundryExtras } from "./truefoundryExtras.js";
45
+ export {
46
+ trueFoundryExtras,
47
+ getTrueFoundryExtras,
48
+ tryGetTrueFoundryExtras,
49
+ } from "./truefoundryExtras.js";
46
50
  export {
47
51
  useTrueFoundryApprovals,
48
52
  useTrueFoundryToolResponses,
@@ -89,7 +89,7 @@ Implemented against Control Plane HTTP (not the gateway SDK):
89
89
 
90
90
  | Method | CP path |
91
91
  | ------ | ------- |
92
- | `getModels` | `GET /api/svc/v1/llm-gateway/model/enabled` |
92
+ | `getModels` | `GET /api/svc/v1/llm-gateway/model/enabled` (+ `GET /api/svc/v1/provider-accounts/providers` for `properties.reasoningEfforts`) |
93
93
  | `getSkills` | `GET /api/ml/v1/agent-skills?include_empty_agent_skills=false` |
94
94
  | `getMcp` | `GET /api/svc/v1/mcp-servers` |
95
95
  | `searchAgents` | `GET /api/svc/v1/agents?type=truefoundry-agent&…` |
@@ -150,7 +150,7 @@ describe("normalizeEnabledModels", () => {
150
150
  expect(rows).toEqual([
151
151
  {
152
152
  id: "openai-main/gpt-4.1",
153
- name: "gpt-4.1",
153
+ name: "openai-main/gpt-4.1",
154
154
  provider: { name: "openai" },
155
155
  properties: {},
156
156
  apiModel: "openai-main/gpt-4.1",
@@ -159,7 +159,7 @@ describe("normalizeEnabledModels", () => {
159
159
  },
160
160
  {
161
161
  id: "anthropic-prod/claude-sonnet-4-6",
162
- name: "claude-sonnet-4-6",
162
+ name: "anthropic-prod/claude-sonnet-4-6",
163
163
  provider: { name: "anthropic" },
164
164
  properties: {},
165
165
  apiModel: "anthropic-prod/claude-sonnet-4-6",
@@ -186,7 +186,7 @@ describe("normalizeEnabledModels", () => {
186
186
  expect(rows).toEqual([
187
187
  {
188
188
  id: "virtual-main/router",
189
- name: "router",
189
+ name: "virtual-main/router",
190
190
  provider: { name: "virtual-model" },
191
191
  properties: {},
192
192
  apiModel: "virtual-main/router",
@@ -13,6 +13,11 @@ import type {
13
13
  SearchAgentSelectorParams,
14
14
  SkillSelectorEntry,
15
15
  } from "../../server/types.js";
16
+ import {
17
+ buildMetadataMap,
18
+ metadataKey,
19
+ reasoningEffortsForModel,
20
+ } from "./modelReasoningEffort.js";
16
21
  import { normalizeAgentSpecForGateway } from "./normalizeAgentSpec.js";
17
22
  import type { TfyAgentSpec } from "./types.js";
18
23
 
@@ -21,7 +26,11 @@ import type { TfyAgentSpec } from "./types.js";
21
26
  // ---------------------------------------------------------------------------
22
27
 
23
28
  export interface TfyModelSelectorEntry extends ModelSelectorEntry {
24
- /** Write into AgentSpec.model.name (model_fqn). */
29
+ /**
30
+ * model_fqn for AgentSpec.model.name.
31
+ * Same value as {@link ModelSelectorEntry.name} / `id` — trueforge-ui writes
32
+ * `name` into the spec; keep this alias for hosts that still read `apiModel`.
33
+ */
25
34
  apiModel: string;
26
35
  modelId: string;
27
36
  providerAccount?: string;
@@ -158,15 +167,17 @@ function isChatModel(row: RawEnabledModel): boolean {
158
167
 
159
168
  function toModelEntry(row: RawEnabledModel): TfyModelSelectorEntry | null {
160
169
  if (!isChatModel(row)) return null;
170
+ // Gateway AgentSpec.model.name must be model_fqn (e.g. account/model-id).
171
+ // trueforge-ui DraftModelSelector writes ModelSelectorEntry.name into that
172
+ // field, so `name` is the FQN — not the CP short display label.
161
173
  const apiModel = row.model_fqn ?? row.id;
162
- const name = row.name;
163
- if (apiModel == null || apiModel === "" || name == null || name === "") {
174
+ if (apiModel == null || apiModel === "") {
164
175
  return null;
165
176
  }
166
- const modelId = row.model_id ?? name;
177
+ const modelId = row.model_id ?? row.name ?? apiModel;
167
178
  return {
168
179
  id: apiModel,
169
- name,
180
+ name: apiModel,
170
181
  provider: { name: row.provider ?? "unknown" },
171
182
  properties: {},
172
183
  apiModel,
@@ -219,14 +230,44 @@ export function normalizeEnabledModels(raw: unknown): TfyModelSelectorEntry[] {
219
230
  return out;
220
231
  }
221
232
 
233
+ /**
234
+ * Attach `properties.reasoningEfforts` from provider-account metadata when the
235
+ * model supports thinking. Pure — used by {@link listEnabledModels} and tests.
236
+ */
237
+ export function enrichModelsWithReasoningEfforts(
238
+ models: TfyModelSelectorEntry[],
239
+ providerMetadataRaw: unknown,
240
+ ): TfyModelSelectorEntry[] {
241
+ const map = buildMetadataMap(providerMetadataRaw);
242
+ if (map.size === 0) return models;
243
+ return models.map((model) => {
244
+ const meta = map.get(metadataKey(model.provider.name, model.modelId));
245
+ const reasoningEfforts = reasoningEffortsForModel(
246
+ meta,
247
+ model.provider.name,
248
+ );
249
+ if (reasoningEfforts == null) return model;
250
+ return {
251
+ ...model,
252
+ properties: { ...model.properties, reasoningEfforts },
253
+ };
254
+ });
255
+ }
256
+
222
257
  export async function listEnabledModels(
223
258
  opts: CpCredentials,
224
259
  ): Promise<TfyModelSelectorEntry[]> {
225
- const raw = await cpFetch<unknown>(
226
- opts,
227
- "/api/svc/v1/llm-gateway/model/enabled",
228
- );
229
- return normalizeEnabledModels(raw);
260
+ // Parallel: providers is enrichment-only; soft-fail keeps models usable.
261
+ const [raw, providers] = await Promise.all([
262
+ cpFetch<unknown>(opts, "/api/svc/v1/llm-gateway/model/enabled"),
263
+ cpFetch<unknown>(
264
+ opts,
265
+ "/api/svc/v1/provider-accounts/providers",
266
+ ).catch(() => null),
267
+ ]);
268
+ const models = normalizeEnabledModels(raw);
269
+ if (providers == null) return models;
270
+ return enrichModelsWithReasoningEfforts(models, providers);
230
271
  }
231
272
 
232
273
  // ---------------------------------------------------------------------------
@@ -0,0 +1,171 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ buildMetadataMap,
5
+ getEffortOptions,
6
+ reasoningEffortsForModel,
7
+ showsReasoningEffort,
8
+ type ModelMetadata,
9
+ } from "./modelReasoningEffort.js";
10
+ import {
11
+ enrichModelsWithReasoningEfforts,
12
+ type TfyModelSelectorEntry,
13
+ } from "./cp.js";
14
+
15
+ const thinkingMeta: ModelMetadata = {
16
+ thinking: true,
17
+ removeParams: ["temperature"],
18
+ params: [
19
+ {
20
+ key: "reasoning_effort",
21
+ type: "string",
22
+ defaultValue: "high",
23
+ supportedValues: ["low", "medium", "high", "max"],
24
+ },
25
+ ],
26
+ };
27
+
28
+ const thinkingViaProviderDefaults: ModelMetadata = {
29
+ thinking: true,
30
+ params: [{ key: "max_tokens", maxValue: 128000 }],
31
+ defaultProviderParams: {
32
+ params: [
33
+ {
34
+ key: "reasoning_effort",
35
+ type: "string",
36
+ defaultValue: "high",
37
+ supportedValues: ["low", "medium", "high", "xhigh", "max"],
38
+ },
39
+ ],
40
+ },
41
+ };
42
+
43
+ describe("showsReasoningEffort", () => {
44
+ it("hides for openai exact slug even when thinking", () => {
45
+ expect(showsReasoningEffort(thinkingMeta, "openai")).toBe(false);
46
+ });
47
+
48
+ it("shows for azure-openai / anthropic when thinking", () => {
49
+ expect(showsReasoningEffort(thinkingMeta, "azure-openai")).toBe(true);
50
+ expect(showsReasoningEffort(thinkingMeta, "anthropic")).toBe(true);
51
+ });
52
+
53
+ it("hides when thinking is off or reasoning_effort is removed", () => {
54
+ expect(
55
+ showsReasoningEffort({ thinking: false }, "anthropic"),
56
+ ).toBe(false);
57
+ expect(
58
+ showsReasoningEffort(
59
+ { thinking: true, removeParams: ["reasoning_effort"] },
60
+ "anthropic",
61
+ ),
62
+ ).toBe(false);
63
+ });
64
+ });
65
+
66
+ describe("getEffortOptions / reasoningEffortsForModel", () => {
67
+ it("uses supportedValues from model params", () => {
68
+ expect(getEffortOptions(thinkingMeta)).toEqual([
69
+ "low",
70
+ "medium",
71
+ "high",
72
+ "max",
73
+ ]);
74
+ expect(reasoningEffortsForModel(thinkingMeta, "anthropic")).toEqual([
75
+ "low",
76
+ "medium",
77
+ "high",
78
+ "max",
79
+ ]);
80
+ });
81
+
82
+ it("falls back to defaultProviderParams", () => {
83
+ expect(getEffortOptions(thinkingViaProviderDefaults)).toEqual([
84
+ "low",
85
+ "medium",
86
+ "high",
87
+ "xhigh",
88
+ "max",
89
+ ]);
90
+ });
91
+
92
+ it("returns undefined when the picker should stay hidden", () => {
93
+ expect(reasoningEffortsForModel(thinkingMeta, "openai")).toBeUndefined();
94
+ expect(
95
+ reasoningEffortsForModel({ thinking: false }, "anthropic"),
96
+ ).toBeUndefined();
97
+ });
98
+ });
99
+
100
+ describe("buildMetadataMap + enrichModelsWithReasoningEfforts", () => {
101
+ const providersPayload = [
102
+ {
103
+ type: "provider-account/anthropic",
104
+ integrations: [
105
+ {
106
+ metadata: {
107
+ "claude-sonnet-4-6": thinkingMeta,
108
+ "claude-haiku": { thinking: false },
109
+ },
110
+ },
111
+ ],
112
+ },
113
+ {
114
+ type: "provider-account/openai",
115
+ integrations: [
116
+ {
117
+ metadata: {
118
+ "o3-mini": thinkingMeta,
119
+ },
120
+ },
121
+ ],
122
+ },
123
+ ];
124
+
125
+ it("indexes metadata by providerSlug/modelId", () => {
126
+ const map = buildMetadataMap(providersPayload);
127
+ expect(map.get("anthropic/claude-sonnet-4-6")).toEqual(thinkingMeta);
128
+ expect(map.get("openai/o3-mini")).toEqual(thinkingMeta);
129
+ });
130
+
131
+ it("attaches properties.reasoningEfforts for thinking models only", () => {
132
+ const models: TfyModelSelectorEntry[] = [
133
+ {
134
+ id: "anthropic-prod/claude-sonnet-4-6",
135
+ name: "anthropic-prod/claude-sonnet-4-6",
136
+ provider: { name: "anthropic" },
137
+ properties: {},
138
+ apiModel: "anthropic-prod/claude-sonnet-4-6",
139
+ modelId: "claude-sonnet-4-6",
140
+ },
141
+ {
142
+ id: "anthropic-prod/claude-haiku",
143
+ name: "anthropic-prod/claude-haiku",
144
+ provider: { name: "anthropic" },
145
+ properties: {},
146
+ apiModel: "anthropic-prod/claude-haiku",
147
+ modelId: "claude-haiku",
148
+ },
149
+ {
150
+ id: "openai-main/o3-mini",
151
+ name: "openai-main/o3-mini",
152
+ provider: { name: "openai" },
153
+ properties: {},
154
+ apiModel: "openai-main/o3-mini",
155
+ modelId: "o3-mini",
156
+ },
157
+ ];
158
+
159
+ const next = enrichModelsWithReasoningEfforts(models, providersPayload);
160
+
161
+ expect(next[0]?.properties.reasoningEfforts).toEqual([
162
+ "low",
163
+ "medium",
164
+ "high",
165
+ "max",
166
+ ]);
167
+ expect(next[1]?.properties.reasoningEfforts).toBeUndefined();
168
+ // exact openai slug hides even when thinking metadata exists
169
+ expect(next[2]?.properties.reasoningEfforts).toBeUndefined();
170
+ });
171
+ });