From f24e3cdae539b985593626e282a9404b9a00c917 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Fri, 30 Jan 2026 15:39:05 -0500 Subject: [PATCH] fix: local updates for PR #4780 Co-authored-by: jlowin --- docs/cli/models.md | 4 + src/cli/cli-utils.ts | 15 ++++ src/cli/models-cli.test.ts | 56 +++++++++++++- src/cli/models-cli.ts | 40 +++++++--- src/commands/models/auth-order.ts | 4 +- src/commands/models/list.status-command.ts | 86 +++++++++++++++------- src/commands/models/list.status.test.ts | 83 +++++++++++++++++---- src/commands/models/shared.ts | 19 +++++ 8 files changed, 250 insertions(+), 57 deletions(-) diff --git a/docs/cli/models.md b/docs/cli/models.md index 07afdb1aa..7b82cc810 100644 --- a/docs/cli/models.md +++ b/docs/cli/models.md @@ -27,6 +27,9 @@ When provider usage snapshots are available, the OAuth/token status section incl provider usage headers. Add `--probe` to run live auth probes against each configured provider profile. Probes are real requests (may consume tokens and trigger rate limits). +Use `--agent ` to inspect a configured agent’s model/auth state. When omitted, +the command uses `OPENCLAW_AGENT_DIR`/`PI_CODING_AGENT_DIR` if set, otherwise the +configured default agent. Notes: - `models set ` accepts `provider/model` or an alias. @@ -44,6 +47,7 @@ Options: - `--probe-timeout ` - `--probe-concurrency ` - `--probe-max-tokens ` +- `--agent ` (configured agent id; overrides `OPENCLAW_AGENT_DIR`/`PI_CODING_AGENT_DIR`) ## Aliases + fallbacks diff --git a/src/cli/cli-utils.ts b/src/cli/cli-utils.ts index 2aae659f8..fe67d2219 100644 --- a/src/cli/cli-utils.ts +++ b/src/cli/cli-utils.ts @@ -1,3 +1,5 @@ +import type { Command } from "commander"; + export type ManagerLookupResult = { manager: T | null; error?: string; @@ -46,3 +48,16 @@ export async function runCommandWithRuntime( runtime.exit(1); } } + +export function resolveOptionFromCommand( + command: Command | undefined, + key: string, +): T | undefined { + let current: Command | null | undefined = command; + while (current) { + const opts = (current.opts?.() ?? {}) as Record; + if (opts[key] !== undefined) return opts[key]; + current = current.parent ?? undefined; + } + return undefined; +} diff --git a/src/cli/models-cli.test.ts b/src/cli/models-cli.test.ts index ba7233874..2a1759b3f 100644 --- a/src/cli/models-cli.test.ts +++ b/src/cli/models-cli.test.ts @@ -1,6 +1,7 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; const githubCopilotLoginCommand = vi.fn(); +const modelsStatusCommand = vi.fn().mockResolvedValue(undefined); vi.mock("../commands/models.js", async () => { const actual = (await vi.importActual( @@ -10,10 +11,16 @@ vi.mock("../commands/models.js", async () => { return { ...actual, githubCopilotLoginCommand, + modelsStatusCommand, }; }); describe("models cli", () => { + beforeEach(() => { + githubCopilotLoginCommand.mockClear(); + modelsStatusCommand.mockClear(); + }); + it("registers github-copilot login command", { timeout: 60_000 }, async () => { const { Command } = await import("commander"); const { registerModelsCli } = await import("./models-cli.js"); @@ -40,4 +47,51 @@ describe("models cli", () => { expect.any(Object), ); }); + + it("passes --agent to models status", async () => { + const { Command } = await import("commander"); + const { registerModelsCli } = await import("./models-cli.js"); + + const program = new Command(); + registerModelsCli(program); + + await program.parseAsync(["models", "status", "--agent", "poe"], { from: "user" }); + + expect(modelsStatusCommand).toHaveBeenCalledWith( + expect.objectContaining({ agent: "poe" }), + expect.any(Object), + ); + }); + + it("passes parent --agent to models status", async () => { + const { Command } = await import("commander"); + const { registerModelsCli } = await import("./models-cli.js"); + + const program = new Command(); + registerModelsCli(program); + + await program.parseAsync(["models", "--agent", "poe", "status"], { from: "user" }); + + expect(modelsStatusCommand).toHaveBeenCalledWith( + expect.objectContaining({ agent: "poe" }), + expect.any(Object), + ); + }); + + it("shows help for models auth without error exit", async () => { + const { Command } = await import("commander"); + const { registerModelsCli } = await import("./models-cli.js"); + + const program = new Command(); + program.exitOverride(); + registerModelsCli(program); + + try { + await program.parseAsync(["models", "auth"], { from: "user" }); + expect.fail("expected help to exit"); + } catch (err) { + const error = err as { exitCode?: number }; + expect(error.exitCode).toBe(0); + } + }); }); diff --git a/src/cli/models-cli.ts b/src/cli/models-cli.ts index b05c91424..653acc127 100644 --- a/src/cli/models-cli.ts +++ b/src/cli/models-cli.ts @@ -29,7 +29,7 @@ import { import { defaultRuntime } from "../runtime.js"; import { formatDocsLink } from "../terminal/links.js"; import { theme } from "../terminal/theme.js"; -import { runCommandWithRuntime } from "./cli-utils.js"; +import { resolveOptionFromCommand, runCommandWithRuntime } from "./cli-utils.js"; function runModelsCommand(action: () => Promise) { return runCommandWithRuntime(defaultRuntime, action); @@ -41,7 +41,10 @@ export function registerModelsCli(program: Command) { .description("Model discovery, scanning, and configuration") .option("--status-json", "Output JSON (alias for `models status --json`)", false) .option("--status-plain", "Plain output (alias for `models status --plain`)", false) - .option("--agent ", "Agent id (default: configured default agent)") + .option( + "--agent ", + "Agent id to inspect (overrides OPENCLAW_AGENT_DIR/PI_CODING_AGENT_DIR)", + ) .addHelpText( "after", () => @@ -86,8 +89,13 @@ export function registerModelsCli(program: Command) { .option("--probe-timeout ", "Per-probe timeout in ms") .option("--probe-concurrency ", "Concurrent probes") .option("--probe-max-tokens ", "Probe max tokens (best-effort)") - .option("--agent ", "Agent id (default: configured default agent)") - .action(async (opts) => { + .option( + "--agent ", + "Agent id to inspect (overrides OPENCLAW_AGENT_DIR/PI_CODING_AGENT_DIR)", + ) + .action(async (opts, command) => { + const agent = + resolveOptionFromCommand(command, "agent") ?? (opts.agent as string | undefined); await runModelsCommand(async () => { await modelsStatusCommand( { @@ -100,7 +108,7 @@ export function registerModelsCli(program: Command) { probeTimeout: opts.probeTimeout as string | undefined, probeConcurrency: opts.probeConcurrency as string | undefined, probeMaxTokens: opts.probeMaxTokens as string | undefined, - agent: opts.agent as string | undefined, + agent, }, defaultRuntime, ); @@ -282,6 +290,10 @@ export function registerModelsCli(program: Command) { }); const auth = models.command("auth").description("Manage model auth profiles"); + auth.option("--agent ", "Agent id for auth order get/set/clear"); + auth.action(() => { + auth.help(); + }); auth .command("add") @@ -375,12 +387,14 @@ export function registerModelsCli(program: Command) { .requiredOption("--provider ", "Provider id (e.g. anthropic)") .option("--agent ", "Agent id (default: configured default agent)") .option("--json", "Output JSON", false) - .action(async (opts) => { + .action(async (opts, command) => { + const agent = + resolveOptionFromCommand(command, "agent") ?? (opts.agent as string | undefined); await runModelsCommand(async () => { await modelsAuthOrderGetCommand( { provider: opts.provider as string, - agent: opts.agent as string | undefined, + agent, json: Boolean(opts.json), }, defaultRuntime, @@ -394,12 +408,14 @@ export function registerModelsCli(program: Command) { .requiredOption("--provider ", "Provider id (e.g. anthropic)") .option("--agent ", "Agent id (default: configured default agent)") .argument("", "Auth profile ids (e.g. anthropic:default)") - .action(async (profileIds: string[], opts) => { + .action(async (profileIds: string[], opts, command) => { + const agent = + resolveOptionFromCommand(command, "agent") ?? (opts.agent as string | undefined); await runModelsCommand(async () => { await modelsAuthOrderSetCommand( { provider: opts.provider as string, - agent: opts.agent as string | undefined, + agent, order: profileIds, }, defaultRuntime, @@ -412,12 +428,14 @@ export function registerModelsCli(program: Command) { .description("Clear per-agent auth order override (fall back to config/round-robin)") .requiredOption("--provider ", "Provider id (e.g. anthropic)") .option("--agent ", "Agent id (default: configured default agent)") - .action(async (opts) => { + .action(async (opts, command) => { + const agent = + resolveOptionFromCommand(command, "agent") ?? (opts.agent as string | undefined); await runModelsCommand(async () => { await modelsAuthOrderClearCommand( { provider: opts.provider as string, - agent: opts.agent as string | undefined, + agent, }, defaultRuntime, ); diff --git a/src/commands/models/auth-order.ts b/src/commands/models/auth-order.ts index 1c5722127..4170ae33a 100644 --- a/src/commands/models/auth-order.ts +++ b/src/commands/models/auth-order.ts @@ -6,9 +6,9 @@ import { } from "../../agents/auth-profiles.js"; import { normalizeProviderId } from "../../agents/model-selection.js"; import { loadConfig } from "../../config/config.js"; -import { normalizeAgentId } from "../../routing/session-key.js"; import type { RuntimeEnv } from "../../runtime.js"; import { shortenHomePath } from "../../utils.js"; +import { resolveKnownAgentId } from "./shared.js"; function resolveTargetAgent( cfg: ReturnType, @@ -17,7 +17,7 @@ function resolveTargetAgent( agentId: string; agentDir: string; } { - const agentId = raw?.trim() ? normalizeAgentId(raw.trim()) : resolveDefaultAgentId(cfg); + const agentId = resolveKnownAgentId({ cfg, rawAgentId: raw }) ?? resolveDefaultAgentId(cfg); const agentDir = resolveAgentDir(cfg, agentId); return { agentId, agentDir }; } diff --git a/src/commands/models/list.status-command.ts b/src/commands/models/list.status-command.ts index ad628f08c..879d37f42 100644 --- a/src/commands/models/list.status-command.ts +++ b/src/commands/models/list.status-command.ts @@ -1,5 +1,10 @@ import path from "node:path"; -import { resolveAgentDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; +import { resolveOpenClawAgentDir } from "../../agents/agent-paths.js"; +import { + resolveAgentDir, + resolveAgentModelFallbacksOverride, + resolveAgentModelPrimary, +} from "../../agents/agent-scope.js"; import { buildAuthHealthSummary, DEFAULT_OAUTH_WARN_MS, @@ -15,6 +20,7 @@ import { buildModelAliasIndex, parseModelRef, resolveConfiguredModelRef, + resolveDefaultModelForAgent, resolveModelRefFromString, } from "../../agents/model-selection.js"; import { CONFIG_PATH, loadConfig } from "../../config/config.js"; @@ -40,8 +46,12 @@ import { sortProbeResults, type AuthProbeSummary, } from "./list.probe.js"; -import { normalizeAgentId } from "../../routing/session-key.js"; -import { DEFAULT_MODEL, DEFAULT_PROVIDER, ensureFlagCompatibility } from "./shared.js"; +import { + DEFAULT_MODEL, + DEFAULT_PROVIDER, + ensureFlagCompatibility, + resolveKnownAgentId, +} from "./shared.js"; export async function modelsStatusCommand( opts: { @@ -63,11 +73,19 @@ export async function modelsStatusCommand( throw new Error("--probe cannot be used with --plain output."); } const cfg = loadConfig(); - const resolved = resolveConfiguredModelRef({ - cfg, - defaultProvider: DEFAULT_PROVIDER, - defaultModel: DEFAULT_MODEL, - }); + const agentId = resolveKnownAgentId({ cfg, rawAgentId: opts.agent }); + const agentDir = agentId ? resolveAgentDir(cfg, agentId) : resolveOpenClawAgentDir(); + const agentModelPrimary = agentId ? resolveAgentModelPrimary(cfg, agentId) : undefined; + const agentFallbacksOverride = agentId + ? resolveAgentModelFallbacksOverride(cfg, agentId) + : undefined; + const resolved = agentId + ? resolveDefaultModelForAgent({ cfg, agentId }) + : resolveConfiguredModelRef({ + cfg, + defaultProvider: DEFAULT_PROVIDER, + defaultModel: DEFAULT_MODEL, + }); const modelConfig = cfg.agents?.defaults?.model as | { primary?: string; fallbacks?: string[] } @@ -77,11 +95,13 @@ export async function modelsStatusCommand( | { primary?: string; fallbacks?: string[] } | string | undefined; - const rawModel = + const rawDefaultsModel = typeof modelConfig === "string" ? modelConfig.trim() : (modelConfig?.primary?.trim() ?? ""); + const rawModel = agentModelPrimary ?? rawDefaultsModel; const resolvedLabel = `${resolved.provider}/${resolved.model}`; const defaultLabel = rawModel || resolvedLabel; - const fallbacks = typeof modelConfig === "object" ? (modelConfig?.fallbacks ?? []) : []; + const defaultsFallbacks = typeof modelConfig === "object" ? (modelConfig?.fallbacks ?? []) : []; + const fallbacks = agentFallbacksOverride ?? defaultsFallbacks; const imageModel = typeof imageConfig === "string" ? imageConfig.trim() : (imageConfig?.primary?.trim() ?? ""); const imageFallbacks = typeof imageConfig === "object" ? (imageConfig?.fallbacks ?? []) : []; @@ -95,10 +115,6 @@ export async function modelsStatusCommand( ); const allowed = Object.keys(cfg.agents?.defaults?.models ?? {}); - const agentId = opts.agent?.trim() - ? normalizeAgentId(opts.agent.trim()) - : resolveDefaultAgentId(cfg); - const agentDir = resolveAgentDir(cfg, agentId); const store = ensureAuthProfileStore(agentDir); const modelsPath = path.join(agentDir, "models.json"); @@ -300,12 +316,21 @@ export async function modelsStatusCommand( JSON.stringify( { configPath: CONFIG_PATH, + ...(agentId ? { agentId } : {}), agentDir, defaultModel: defaultLabel, resolvedDefault: resolvedLabel, fallbacks, imageModel: imageModel || null, imageFallbacks, + ...(agentId + ? { + modelConfig: { + defaultSource: agentModelPrimary ? "agent" : "defaults", + fallbacksSource: agentFallbacksOverride !== undefined ? "agent" : "defaults", + }, + } + : {}), aliases, allowed, auth: { @@ -341,7 +366,10 @@ export async function modelsStatusCommand( } const rich = isRich(opts); + type ModelConfigSource = "agent" | "defaults"; const label = (value: string) => colorize(rich, theme.accent, value.padEnd(14)); + const labelWithSource = (value: string, source?: ModelConfigSource) => + label(source ? `${value} (${source})` : value); const displayDefault = rawModel && rawModel !== resolvedLabel ? `${resolvedLabel} (from ${rawModel})` : resolvedLabel; @@ -356,32 +384,34 @@ export async function modelsStatusCommand( )}`, ); runtime.log( - `${label("Default")}${colorize(rich, theme.muted, ":")} ${colorize( + `${labelWithSource("Default", agentId ? (agentModelPrimary ? "agent" : "defaults") : undefined)}${colorize( rich, - theme.success, - displayDefault, - )}`, + theme.muted, + ":", + )} ${colorize(rich, theme.success, displayDefault)}`, ); runtime.log( - `${label(`Fallbacks (${fallbacks.length || 0})`)}${colorize(rich, theme.muted, ":")} ${colorize( + `${labelWithSource( + `Fallbacks (${fallbacks.length || 0})`, + agentId ? (agentFallbacksOverride !== undefined ? "agent" : "defaults") : undefined, + )}${colorize(rich, theme.muted, ":")} ${colorize( rich, fallbacks.length ? theme.warn : theme.muted, fallbacks.length ? fallbacks.join(", ") : "-", )}`, ); runtime.log( - `${label("Image model")}${colorize(rich, theme.muted, ":")} ${colorize( - rich, - imageModel ? theme.accentBright : theme.muted, - imageModel || "-", - )}`, - ); - runtime.log( - `${label(`Image fallbacks (${imageFallbacks.length || 0})`)}${colorize( + `${labelWithSource("Image model", agentId ? "defaults" : undefined)}${colorize( rich, theme.muted, ":", - )} ${colorize( + )} ${colorize(rich, imageModel ? theme.accentBright : theme.muted, imageModel || "-")}`, + ); + runtime.log( + `${labelWithSource( + `Image fallbacks (${imageFallbacks.length || 0})`, + agentId ? "defaults" : undefined, + )}${colorize(rich, theme.muted, ":")} ${colorize( rich, imageFallbacks.length ? theme.accentBright : theme.muted, imageFallbacks.length ? imageFallbacks.join(", ") : "-", diff --git a/src/commands/models/list.status.test.ts b/src/commands/models/list.status.test.ts index 48d11e46a..daa9aa232 100644 --- a/src/commands/models/list.status.test.ts +++ b/src/commands/models/list.status.test.ts @@ -29,8 +29,11 @@ const mocks = vi.hoisted(() => { return { store, + resolveOpenClawAgentDir: vi.fn().mockReturnValue("/tmp/openclaw-agent"), resolveAgentDir: vi.fn().mockReturnValue("/tmp/openclaw-agent"), - resolveDefaultAgentId: vi.fn().mockReturnValue("main"), + resolveAgentModelPrimary: vi.fn().mockReturnValue(undefined), + resolveAgentModelFallbacksOverride: vi.fn().mockReturnValue(undefined), + listAgentIds: vi.fn().mockReturnValue(["main", "jeremiah"]), ensureAuthProfileStore: vi.fn().mockReturnValue(store), listProfilesForProvider: vi.fn((s: typeof store, provider: string) => { return Object.entries(s.profiles) @@ -72,18 +75,16 @@ const mocks = vi.hoisted(() => { }; }); -vi.mock("../../agents/agent-scope.js", () => ({ - resolveAgentDir: mocks.resolveAgentDir, - resolveDefaultAgentId: mocks.resolveDefaultAgentId, +vi.mock("../../agents/agent-paths.js", () => ({ + resolveOpenClawAgentDir: mocks.resolveOpenClawAgentDir, })); -vi.mock("../../routing/session-key.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - normalizeAgentId: (id: string) => id.toLowerCase().replace(/\s+/g, "-"), - }; -}); +vi.mock("../../agents/agent-scope.js", () => ({ + resolveAgentDir: mocks.resolveAgentDir, + resolveAgentModelPrimary: mocks.resolveAgentModelPrimary, + resolveAgentModelFallbacksOverride: mocks.resolveAgentModelFallbacksOverride, + listAgentIds: mocks.listAgentIds, +})); vi.mock("../../agents/auth-profiles.js", async (importOriginal) => { const actual = await importOriginal(); @@ -127,6 +128,7 @@ describe("modelsStatusCommand auth overview", () => { await modelsStatusCommand({ json: true }, runtime as never); const payload = JSON.parse(String((runtime.log as vi.Mock).mock.calls[0][0])); + expect(mocks.resolveOpenClawAgentDir).toHaveBeenCalled(); expect(payload.defaultModel).toBe("anthropic/claude-opus-4-5"); expect(payload.auth.storePath).toBe("/tmp/openclaw-agent/auth-profiles.json"); expect(payload.auth.shellEnvFallback.enabled).toBe(true); @@ -157,23 +159,74 @@ describe("modelsStatusCommand auth overview", () => { ).toBe(true); }); - it("resolves agent dir from --agent flag", async () => { - mocks.resolveAgentDir.mockReturnValue("/tmp/openclaw-agent-custom"); + it("uses agent overrides and reports sources", async () => { const localRuntime = { log: vi.fn(), error: vi.fn(), exit: vi.fn(), }; + const originalPrimary = mocks.resolveAgentModelPrimary.getMockImplementation(); + const originalFallbacks = mocks.resolveAgentModelFallbacksOverride.getMockImplementation(); + const originalAgentDir = mocks.resolveAgentDir.getMockImplementation(); + + mocks.resolveAgentModelPrimary.mockReturnValue("openai/gpt-4"); + mocks.resolveAgentModelFallbacksOverride.mockReturnValue(["openai/gpt-3.5"]); + mocks.resolveAgentDir.mockReturnValue("/tmp/openclaw-agent-custom"); + try { - await modelsStatusCommand({ json: true, agent: "jeremiah" }, localRuntime as never); + await modelsStatusCommand({ json: true, agent: "Jeremiah" }, localRuntime as never); expect(mocks.resolveAgentDir).toHaveBeenCalledWith(expect.anything(), "jeremiah"); const payload = JSON.parse(String((localRuntime.log as vi.Mock).mock.calls[0][0])); + expect(payload.agentId).toBe("jeremiah"); expect(payload.agentDir).toBe("/tmp/openclaw-agent-custom"); + expect(payload.defaultModel).toBe("openai/gpt-4"); + expect(payload.fallbacks).toEqual(["openai/gpt-3.5"]); + expect(payload.modelConfig).toEqual({ + defaultSource: "agent", + fallbacksSource: "agent", + }); } finally { - mocks.resolveAgentDir.mockReturnValue("/tmp/openclaw-agent"); + mocks.resolveAgentModelPrimary.mockImplementation(originalPrimary); + mocks.resolveAgentModelFallbacksOverride.mockImplementation(originalFallbacks); + mocks.resolveAgentDir.mockImplementation(originalAgentDir); } }); + it("labels defaults when --agent has no overrides", async () => { + const localRuntime = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + }; + const originalPrimary = mocks.resolveAgentModelPrimary.getMockImplementation(); + const originalFallbacks = mocks.resolveAgentModelFallbacksOverride.getMockImplementation(); + + mocks.resolveAgentModelPrimary.mockReturnValue(undefined); + mocks.resolveAgentModelFallbacksOverride.mockReturnValue(undefined); + + try { + await modelsStatusCommand({ agent: "main" }, localRuntime as never); + const output = (localRuntime.log as vi.Mock).mock.calls + .map((call) => String(call[0])) + .join("\n"); + expect(output).toContain("Default (defaults)"); + expect(output).toContain("Fallbacks (0) (defaults)"); + } finally { + mocks.resolveAgentModelPrimary.mockImplementation(originalPrimary); + mocks.resolveAgentModelFallbacksOverride.mockImplementation(originalFallbacks); + } + }); + + it("throws when agent id is unknown", async () => { + const localRuntime = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + }; + await expect(modelsStatusCommand({ agent: "unknown" }, localRuntime as never)).rejects.toThrow( + 'Unknown agent id "unknown".', + ); + }); it("exits non-zero when auth is missing", async () => { const originalProfiles = { ...mocks.store.profiles }; mocks.store.profiles = {}; diff --git a/src/commands/models/shared.ts b/src/commands/models/shared.ts index 823ba5ed5..4f4268361 100644 --- a/src/commands/models/shared.ts +++ b/src/commands/models/shared.ts @@ -5,11 +5,14 @@ import { parseModelRef, resolveModelRefFromString, } from "../../agents/model-selection.js"; +import { listAgentIds } from "../../agents/agent-scope.js"; +import { formatCliCommand } from "../../cli/command-format.js"; import { type OpenClawConfig, readConfigFileSnapshot, writeConfigFile, } from "../../config/config.js"; +import { normalizeAgentId } from "../../routing/session-key.js"; export const ensureFlagCompatibility = (opts: { json?: boolean; plain?: boolean }) => { if (opts.json && opts.plain) { @@ -82,6 +85,22 @@ export function normalizeAlias(alias: string): string { return trimmed; } +export function resolveKnownAgentId(params: { + cfg: OpenClawConfig; + rawAgentId?: string | null; +}): string | undefined { + const raw = params.rawAgentId?.trim(); + if (!raw) return undefined; + const agentId = normalizeAgentId(raw); + const knownAgents = listAgentIds(params.cfg); + if (!knownAgents.includes(agentId)) { + throw new Error( + `Unknown agent id "${raw}". Use "${formatCliCommand("openclaw agents list")}" to see configured agents.`, + ); + } + return agentId; +} + export { modelKey }; export { DEFAULT_MODEL, DEFAULT_PROVIDER };