From c5af4f99e1ac57a0f0e1901e19cfcfb7e19ad961 Mon Sep 17 00:00:00 2001 From: Gab Date: Sat, 4 Apr 2026 17:56:41 +1100 Subject: [PATCH] feat: tfcode --- bun.lock | 2 +- packages/tfcode/bin/tfcode.js | 5 + packages/tfcode/package.json | 8 +- packages/tfcode/python/tf_sync/__init__.py | 21 ++ .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 661 bytes .../__pycache__/agents.cpython-313.pyc | Bin 0 -> 1472 bytes .../__pycache__/config.cpython-313.pyc | Bin 0 -> 8457 bytes .../tf_sync/__pycache__/mcp.cpython-313.pyc | Bin 0 -> 1645 bytes .../tf_sync/__pycache__/tools.cpython-313.pyc | Bin 0 -> 9569 bytes packages/tfcode/python/tf_sync/agents.py | 39 +++ packages/tfcode/python/tf_sync/config.py | 221 ++++++++++++++ packages/tfcode/python/tf_sync/mcp.py | 41 +++ packages/tfcode/python/tf_sync/tools.py | 282 ++++++++++++++++++ packages/tfcode/script/publish-tfcode.ts | 4 + packages/tfcode/src/cli/cmd/tools.ts | 16 +- packages/tfcode/src/cli/upgrade.ts | 9 +- packages/tfcode/src/provider/transform.ts | 5 - 17 files changed, 643 insertions(+), 10 deletions(-) create mode 100644 packages/tfcode/python/tf_sync/__init__.py create mode 100644 packages/tfcode/python/tf_sync/__pycache__/__init__.cpython-313.pyc create mode 100644 packages/tfcode/python/tf_sync/__pycache__/agents.cpython-313.pyc create mode 100644 packages/tfcode/python/tf_sync/__pycache__/config.cpython-313.pyc create mode 100644 packages/tfcode/python/tf_sync/__pycache__/mcp.cpython-313.pyc create mode 100644 packages/tfcode/python/tf_sync/__pycache__/tools.cpython-313.pyc create mode 100644 packages/tfcode/python/tf_sync/agents.py create mode 100644 packages/tfcode/python/tf_sync/config.py create mode 100644 packages/tfcode/python/tf_sync/mcp.py create mode 100644 packages/tfcode/python/tf_sync/tools.py diff --git a/bun.lock b/bun.lock index 1d789519a..621bd8822 100644 --- a/bun.lock +++ b/bun.lock @@ -381,7 +381,7 @@ }, "packages/tfcode": { "name": "@toothfairyai/tfcode", - "version": "1.0.23", + "version": "1.0.26", "bin": { "tfcode": "./bin/tfcode", }, diff --git a/packages/tfcode/bin/tfcode.js b/packages/tfcode/bin/tfcode.js index 5ba9c022d..5e844a18c 100755 --- a/packages/tfcode/bin/tfcode.js +++ b/packages/tfcode/bin/tfcode.js @@ -68,9 +68,14 @@ function runPythonSync(method, config = null) { const apiKey = config?.api_key || process.env.TF_API_KEY || "" const region = config?.region || process.env.TF_REGION || "au" + // Add embedded python path to sys.path + const embeddedPythonPath = join(__dirname, "..", "python") + const pythonCode = ` import json, sys, os try: + # Add embedded tf_sync module path + sys.path.insert(0, "${embeddedPythonPath}") os.environ["TF_WORKSPACE_ID"] = "${wsId}" os.environ["TF_API_KEY"] = "${apiKey}" os.environ["TF_REGION"] = "${region}" diff --git a/packages/tfcode/package.json b/packages/tfcode/package.json index e26ca849a..72f576547 100644 --- a/packages/tfcode/package.json +++ b/packages/tfcode/package.json @@ -1,9 +1,15 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.0.23", + "version": "1.0.26", "name": "@toothfairyai/tfcode", "type": "module", "license": "MIT", + "files": [ + "bin", + "python", + "postinstall.mjs", + "LICENSE" + ], "scripts": { "prepare": "effect-language-service patch || true", "postinstall": "node scripts/postinstall.cjs", diff --git a/packages/tfcode/python/tf_sync/__init__.py b/packages/tfcode/python/tf_sync/__init__.py new file mode 100644 index 000000000..9840670fb --- /dev/null +++ b/packages/tfcode/python/tf_sync/__init__.py @@ -0,0 +1,21 @@ +""" +tf-sync: ToothFairyAI workspace sync layer for tfcode +""" + +from tf_sync.agents import sync_agents +from tf_sync.mcp import sync_mcp_servers +from tf_sync.tools import sync_tools, ToolType +from tf_sync.config import TFConfig, load_config, validate_credentials, get_region_urls + +__all__ = [ + "sync_agents", + "sync_mcp_servers", + "sync_tools", + "ToolType", + "TFConfig", + "load_config", + "validate_credentials", + "get_region_urls", +] + +__version__ = "0.1.0" \ No newline at end of file diff --git a/packages/tfcode/python/tf_sync/__pycache__/__init__.cpython-313.pyc b/packages/tfcode/python/tf_sync/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d33551a85502a3064bcf42fba2400a0cd1e8b7b6 GIT binary patch literal 661 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 literal 0 HcmV?d00001 diff --git a/packages/tfcode/python/tf_sync/__pycache__/tools.cpython-313.pyc b/packages/tfcode/python/tf_sync/__pycache__/tools.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1c5816ce83bc90d77caae5a3004f2182b803f6c9 GIT binary patch literal 9569 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 AgentSyncResult: + """ + Sync agents from ToothFairyAI workspace. + + NOTE: Currently not implemented. Reserved for future use. + + Args: + config: TFConfig instance + + Returns: + AgentSyncResult (currently always returns not implemented) + """ + return AgentSyncResult( + success=False, + error="Agent sync not yet implemented. Use tools sync for now.", + ) \ No newline at end of file diff --git a/packages/tfcode/python/tf_sync/config.py b/packages/tfcode/python/tf_sync/config.py new file mode 100644 index 000000000..fc4a384b6 --- /dev/null +++ b/packages/tfcode/python/tf_sync/config.py @@ -0,0 +1,221 @@ +""" +Configuration management for tfcode ToothFairyAI integration. +Uses the official ToothFairyAI Python SDK for multi-region support. +""" + +import os +from enum import Enum +from typing import Optional + +from pydantic import BaseModel, Field, SecretStr +from toothfairyai import ToothFairyClient +from toothfairyai.errors import ToothFairyError + + +class Region(str, Enum): + DEV = "dev" + AU = "au" + EU = "eu" + US = "us" + + +class ToolType(str, Enum): + MCP_SERVER = "mcp_server" + AGENT_SKILL = "agent_skill" + CODER_AGENT = "coder_agent" + DATABASE_SCRIPT = "database_script" + API_FUNCTION = "api_function" + PROMPT = "prompt" + + +class FunctionRequestType(str, Enum): + GET = "get" + POST = "post" + PUT = "put" + DELETE = "delete" + PATCH = "patch" + CUSTOM = "custom" + GRAPHQL_QUERY = "graphql_query" + GRAPHQL_MUTATION = "graphql_mutation" + + +# Region-specific URL configurations +REGION_URLS = { + Region.DEV: { + "base_url": "https://api.toothfairylab.link", + "ai_url": "https://ai.toothfairylab.link", + "ai_stream_url": "https://ais.toothfairylab.link", + "mcp_url": "https://mcp.toothfairylab.link/sse", + "mcp_proxy_url": "https://mcp-proxy.toothfairylab.link", + }, + Region.AU: { + "base_url": "https://api.toothfairyai.com", + "ai_url": "https://ai.toothfairyai.com", + "ai_stream_url": "https://ais.toothfairyai.com", + "mcp_url": "https://mcp.toothfairyai.com/sse", + "mcp_proxy_url": "https://mcp-proxy.toothfairyai.com", + }, + Region.EU: { + "base_url": "https://api.eu.toothfairyai.com", + "ai_url": "https://ai.eu.toothfairyai.com", + "ai_stream_url": "https://ais.eu.toothfairyai.com", + "mcp_url": "https://mcp.eu.toothfairyai.com/sse", + "mcp_proxy_url": "https://mcp-proxy.eu.toothfairyai.com", + }, + Region.US: { + "base_url": "https://api.us.toothfairyai.com", + "ai_url": "https://ai.us.toothfairyai.com", + "ai_stream_url": "https://ais.us.toothfairyai.com", + "mcp_url": "https://mcp.us.toothfairyai.com/sse", + "mcp_proxy_url": "https://mcp-proxy.us.toothfairyai.com", + }, +} + + +def get_region_urls(region: Region) -> dict[str, str]: + """Get URLs for a specific region.""" + return REGION_URLS.get(region, REGION_URLS[Region.AU]) + + +class TFConfig(BaseModel): + """ToothFairyAI workspace configuration.""" + + workspace_id: str + api_key: SecretStr + region: Region = Region.AU + enabled: bool = True + + sync_interval: int = Field(default=3600, ge=60) + mcp_proxy_timeout: int = Field(default=30000, ge=1000) + + _client: Optional[ToothFairyClient] = None + + def get_client(self) -> ToothFairyClient: + """ + Get or create a ToothFairyClient instance configured for this region. + + Returns: + ToothFairyClient configured with region-specific URLs + """ + if self._client is None: + urls = get_region_urls(self.region) + self._client = ToothFairyClient( + api_key=self.api_key.get_secret_value(), + workspace_id=self.workspace_id, + base_url=urls["base_url"], + ai_url=urls["ai_url"], + ai_stream_url=urls["ai_stream_url"], + ) + return self._client + + @property + def mcp_sse_url(self) -> str: + """Get the MCP SSE endpoint URL for this region.""" + return get_region_urls(self.region)["mcp_url"] + + @property + def mcp_proxy_url(self) -> str: + """Get the MCP proxy URL for this region.""" + return get_region_urls(self.region)["mcp_proxy_url"] + + +class CredentialValidationResult(BaseModel): + """Result of credential validation.""" + success: bool + workspace_id: Optional[str] = None + workspace_name: Optional[str] = None + error: Optional[str] = None + + +def load_config( + workspace_id: Optional[str] = None, + api_key: Optional[str] = None, + region: Optional[Region] = None, +) -> TFConfig: + """ + Load ToothFairyAI configuration from environment or parameters. + + Args: + workspace_id: Workspace UUID (defaults to TF_WORKSPACE_ID env var) + api_key: API key (defaults to TF_API_KEY env var) + region: Region (defaults to TF_REGION env var or 'au') + + Returns: + TFConfig instance + + Raises: + ValueError: If required configuration is missing + """ + ws_id = workspace_id or os.environ.get("TF_WORKSPACE_ID") + key = api_key or os.environ.get("TF_API_KEY") + + # Parse region from env or use provided/default + region_str = os.environ.get("TF_REGION", "au") + reg = region or Region(region_str) + + if not ws_id: + raise ValueError("TF_WORKSPACE_ID not set. Set environment variable or pass workspace_id.") + if not key: + raise ValueError("TF_API_KEY not set. Set environment variable or pass api_key.") + + return TFConfig( + workspace_id=ws_id, + api_key=SecretStr(key), + region=reg, + ) + + +def validate_credentials(config: TFConfig) -> CredentialValidationResult: + """ + Validate ToothFairyAI credentials using the SDK. + + Args: + config: TFConfig instance + + Returns: + CredentialValidationResult indicating success or failure + """ + try: + client = config.get_client() + + # Test connection by listing chats (lightweight operation) + if client.test_connection(): + return CredentialValidationResult( + success=True, + workspace_id=config.workspace_id, + workspace_name="Connected", + ) + else: + return CredentialValidationResult( + success=False, + error="Connection test failed. Check credentials and region.", + ) + + except ToothFairyError as e: + error_msg = str(e) + + if "401" in error_msg or "Unauthorized" in error_msg: + return CredentialValidationResult( + success=False, + error="Invalid API key. Check TF_API_KEY environment variable.", + ) + elif "403" in error_msg or "Forbidden" in error_msg: + return CredentialValidationResult( + success=False, + error="API access not allowed. Business or Enterprise subscription required.", + ) + elif "404" in error_msg or "Not Found" in error_msg: + return CredentialValidationResult( + success=False, + error="Workspace not found. Check TF_WORKSPACE_ID environment variable.", + ) + else: + return CredentialValidationResult( + success=False, + error=f"API error: {error_msg}", + ) + except Exception as e: + return CredentialValidationResult( + success=False, + error=f"Unexpected error: {str(e)}", + ) \ No newline at end of file diff --git a/packages/tfcode/python/tf_sync/mcp.py b/packages/tfcode/python/tf_sync/mcp.py new file mode 100644 index 000000000..564d02d37 --- /dev/null +++ b/packages/tfcode/python/tf_sync/mcp.py @@ -0,0 +1,41 @@ +""" +MCP server sync module for tfcode. + +NOTE: MCP servers are not currently exposed via the ToothFairyAI SDK. +This module is reserved for future implementation when MCP server +discovery is added to the SDK. + +For now, MCP servers should be configured manually via tfcode.json. +""" + +from pydantic import BaseModel + +from tf_sync.config import TFConfig +from tf_sync.tools import SyncedTool, ToolType + + +class MCPServerSyncResult(BaseModel): + """Result of MCP server sync operation.""" + + success: bool + servers: list[SyncedTool] = [] + error: str | None = None + + +def sync_mcp_servers(config: TFConfig) -> MCPServerSyncResult: + """ + Sync MCP servers from ToothFairyAI workspace. + + NOTE: Currently not supported. MCP servers are not exposed via the SDK. + Configure MCP servers manually in tfcode.json instead. + + Args: + config: TFConfig instance + + Returns: + MCPServerSyncResult with error message + """ + return MCPServerSyncResult( + success=False, + error="MCP server sync not available via SDK. Configure MCP servers in tfcode.json.", + ) \ No newline at end of file diff --git a/packages/tfcode/python/tf_sync/tools.py b/packages/tfcode/python/tf_sync/tools.py new file mode 100644 index 000000000..db8057c86 --- /dev/null +++ b/packages/tfcode/python/tf_sync/tools.py @@ -0,0 +1,282 @@ +""" +Tool sync module for tfcode. +Syncs tools from ToothFairyAI workspace using the official SDK. + +SDK Structure: +- agent_functions: API Functions (with request_type) +- connections: Provider connections (openai, anthropic, etc.) +- agents: TF workspace agents +- prompts: Prompt templates (with available_to_agents mapping) +""" + +from typing import Any, Optional, List + +from pydantic import BaseModel +from toothfairyai.types import AgentFunction + +from tf_sync.config import TFConfig, ToolType, FunctionRequestType + + +class SyncedTool(BaseModel): + """A tool synced from ToothFairyAI workspace.""" + + id: str + name: str + description: Optional[str] = None + tool_type: ToolType + + is_mcp_server: bool = False + is_agent_skill: bool = False + is_database_script: bool = False + + request_type: Optional[FunctionRequestType] = None + url: Optional[str] = None + tools: list[str] = [] + + authorisation_type: Optional[str] = None + + auth_via: str = "tf_proxy" + + # Coder agent specific fields for prompting/model configuration + interpolation_string: Optional[str] = None + goals: Optional[str] = None + temperature: Optional[float] = None + max_tokens: Optional[int] = None + llm_base_model: Optional[str] = None + llm_provider: Optional[str] = None + + +class SyncedPrompt(BaseModel): + """A prompt template synced from ToothFairyAI workspace.""" + + id: str + label: str + interpolation_string: str + prompt_type: Optional[str] = None + available_to_agents: Optional[List[str]] = None + description: Optional[str] = None + + +class ToolSyncResult(BaseModel): + """Result of tool sync operation.""" + + success: bool + tools: list[SyncedTool] = [] + prompts: list[SyncedPrompt] = [] + by_type: dict[str, int] = {} + error: Optional[str] = None + + +def classify_tool(func: AgentFunction) -> ToolType: + """ + Classify a tool based on its properties. + + Types: + - AGENT_SKILL: is_agent_skill=True + - API_FUNCTION: has request_type + + Args: + func: AgentFunction from TF SDK + + Returns: + ToolType enum value + """ + # Agent skills have is_agent_skill=True + if getattr(func, 'is_agent_skill', None) is True: + return ToolType.AGENT_SKILL + + # All agent_functions with request_type are API Functions + if func.request_type: + return ToolType.API_FUNCTION + + return ToolType.API_FUNCTION + + +def parse_function(func: AgentFunction) -> SyncedTool: + """ + Parse AgentFunction from SDK into SyncedTool. + + Args: + func: AgentFunction from TF SDK + + Returns: + SyncedTool instance + """ + tool_type = classify_tool(func) + + request_type_enum = None + if func.request_type: + try: + request_type_enum = FunctionRequestType(func.request_type) + except ValueError: + pass + + # API Functions may have user-provided auth (authorisation_type) + # or may use TF proxy + auth_via = "user_provided" if func.authorisation_type == "api_key" else "tf_proxy" + + # Agent skills use skill script + if tool_type == ToolType.AGENT_SKILL: + auth_via = "tf_skill" + + return SyncedTool( + id=func.id, + name=func.name, + description=func.description, + tool_type=tool_type, + request_type=request_type_enum, + url=func.url, + authorisation_type=func.authorisation_type, + auth_via=auth_via, + is_agent_skill=tool_type == ToolType.AGENT_SKILL, + ) + + +def parse_agent(agent) -> SyncedTool: + """ + Parse Agent from SDK into SyncedTool. + + Coder agents (mode='coder') are CODER_AGENT type, not skills. + + Args: + agent: Agent from TF SDK + + Returns: + SyncedTool instance with full agent configuration + """ + return SyncedTool( + id=agent.id, + name=agent.label or f"agent_{agent.id[:8]}", + description=agent.description, + tool_type=ToolType.CODER_AGENT, + is_agent_skill=False, + auth_via="tf_agent", + # Agent prompting configuration + interpolation_string=getattr(agent, 'interpolation_string', None), + goals=getattr(agent, 'goals', None), + # Agent model configuration + temperature=getattr(agent, 'temperature', None), + max_tokens=getattr(agent, 'max_tokens', None), + llm_base_model=getattr(agent, 'llm_base_model', None), + llm_provider=getattr(agent, 'llm_provider', None), + ) + + +def parse_prompt(prompt) -> SyncedPrompt: + """ + Parse Prompt from SDK into SyncedPrompt. + + Args: + prompt: Prompt from TF SDK + + Returns: + SyncedPrompt instance + """ + return SyncedPrompt( + id=prompt.id, + label=prompt.label, + interpolation_string=prompt.interpolation_string, + prompt_type=getattr(prompt, 'prompt_type', None), + available_to_agents=getattr(prompt, 'available_to_agents', None), + description=getattr(prompt, 'description', None), + ) + + +def sync_tools(config: TFConfig) -> ToolSyncResult: + """ + Sync all tools from ToothFairyAI workspace using SDK. + + Includes: + - Agent Functions (API Functions with request_type) + - Agent Skills (functions with is_agent_skill=True) + - Coder Agents (agents with mode='coder') + - Prompts (prompt templates with available_to_agents mapping) + + Args: + config: TFConfig instance + + Returns: + ToolSyncResult with synced tools and prompts + """ + try: + client = config.get_client() + + # Sync agent functions (API auto-paginates up to 5000) + func_result = client.agent_functions.list() + tools = [parse_function(f) for f in func_result.items] + + # Sync coder agents (API auto-paginates up to 5000) + try: + agents_result = client.agents.list() + for agent in agents_result.items: + if getattr(agent, 'mode', None) == 'coder': + tools.append(parse_agent(agent)) + except Exception: + pass + + # Sync prompts (API auto-paginates up to 5000) + prompts = [] + try: + prompts_result = client.prompts.list() + prompts = [parse_prompt(p) for p in prompts_result.items] + except Exception: + pass + + by_type = {} + for tool in tools: + type_name = tool.tool_type.value + by_type[type_name] = by_type.get(type_name, 0) + 1 + + if prompts: + by_type['prompt'] = len(prompts) + + return ToolSyncResult( + success=True, + tools=tools, + prompts=prompts, + by_type=by_type, + ) + + except Exception as e: + return ToolSyncResult( + success=False, + error=f"Sync failed: {str(e)}", + ) + + +def sync_tools_by_type( + config: TFConfig, + tool_types: Optional[list[ToolType]] = None, +) -> ToolSyncResult: + """ + Sync tools of specific types from ToothFairyAI workspace. + + Args: + config: TFConfig instance + tool_types: List of ToolType to sync (None = all) + + Returns: + ToolSyncResult with filtered tools + """ + result = sync_tools(config) + + if not result.success or not tool_types: + return result + + filtered = [t for t in result.tools if t.tool_type in tool_types] + + by_type = {} + for tool in filtered: + type_name = tool.tool_type.value + by_type[type_name] = by_type.get(type_name, 0) + 1 + + return ToolSyncResult( + success=True, + tools=filtered, + by_type=by_type, + ) + + +def sync_api_functions_only(config: TFConfig) -> ToolSyncResult: + """Sync only API Functions (has requestType).""" + return sync_tools_by_type(config, [ToolType.API_FUNCTION]) \ No newline at end of file diff --git a/packages/tfcode/script/publish-tfcode.ts b/packages/tfcode/script/publish-tfcode.ts index 6c69054be..d5ff7c047 100644 --- a/packages/tfcode/script/publish-tfcode.ts +++ b/packages/tfcode/script/publish-tfcode.ts @@ -110,6 +110,10 @@ async function createMainPackage() { await Bun.file(`./dist/tfcode/postinstall.mjs`).write(await Bun.file("./script/postinstall-tfcode.mjs").text()) await Bun.file(`./dist/tfcode/LICENSE`).write(await Bun.file("../../LICENSE").text()) + // Copy embedded python module (tf_sync) + await $`cp -r ./python ./dist/tfcode/` + console.log("Copied embedded python/tf_sync module") + // Copy the current platform's binary to the main package // This makes installation faster (no need to download or copy from optionalDependencies) const currentPlatform = process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : "windows" diff --git a/packages/tfcode/src/cli/cmd/tools.ts b/packages/tfcode/src/cli/cmd/tools.ts index 2b35ff475..e44c3572a 100644 --- a/packages/tfcode/src/cli/cmd/tools.ts +++ b/packages/tfcode/src/cli/cmd/tools.ts @@ -64,11 +64,20 @@ const TFCODE_CONFIG_DIR = ".tfcode" const TFCODE_TOOLS_FILE = "tools.json" function getPythonSyncPath(): string { - const possible = [ + // Check embedded python path first (for npm distribution) + const embedded = [ + path.join(__dirname, "..", "..", "..", "..", "python"), // packages/tfcode/python + path.join(__dirname, "..", "..", "..", "python"), // dist/python + ] + for (const p of embedded) { + if (existsSync(p)) return p + } + // Fallback to development paths + const dev = [ path.join(__dirname, "..", "..", "..", "..", "tf-sync", "src", "tf_sync"), path.join(process.cwd(), "packages", "tf-sync", "src", "tf_sync"), ] - for (const p of possible) { + for (const p of dev) { if (existsSync(p)) return p } return "tf_sync" @@ -76,6 +85,7 @@ function getPythonSyncPath(): string { async function runPythonSync(method: string, args: Record = {}): Promise { const credentials = await loadCredentials() + const pythonPath = getPythonSyncPath() const pythonCode = ` import json @@ -83,6 +93,8 @@ import sys import os try: + # Add embedded tf_sync module path + sys.path.insert(0, "${pythonPath.replace(/\\/g, "/")}") from tf_sync.config import load_config, validate_credentials from tf_sync.tools import sync_tools, sync_tools_by_type, ToolType from tf_sync.mcp import sync_mcp_servers diff --git a/packages/tfcode/src/cli/upgrade.ts b/packages/tfcode/src/cli/upgrade.ts index 7acc4fa27..8ea912df3 100644 --- a/packages/tfcode/src/cli/upgrade.ts +++ b/packages/tfcode/src/cli/upgrade.ts @@ -16,10 +16,17 @@ export async function upgrade() { if (Installation.VERSION === latest) return - if (config.autoupdate === false || Flag.OPENCODE_DISABLE_AUTOUPDATE) return + if (config.autoupdate === false) return const kind = Installation.getReleaseType(Installation.VERSION, latest) + // When auto-upgrade is disabled (e.g. tfcode uses npm for updates), + // still notify the user that a new version is available. + if (Flag.OPENCODE_DISABLE_AUTOUPDATE) { + await Bus.publish(Installation.Event.UpdateAvailable, { version: latest }) + return + } + if (config.autoupdate === "notify" || kind !== "patch") { await Bus.publish(Installation.Event.UpdateAvailable, { version: latest }) return diff --git a/packages/tfcode/src/provider/transform.ts b/packages/tfcode/src/provider/transform.ts index 914397957..b6cfe8b21 100644 --- a/packages/tfcode/src/provider/transform.ts +++ b/packages/tfcode/src/provider/transform.ts @@ -340,17 +340,12 @@ export namespace ProviderTransform { if ( id.includes("deepseek") || id.includes("minimax") || - id.includes("glm") || id.includes("mistral") || id.includes("kimi") || - // TODO: Remove this after models.dev data is fixed to use "kimi-k2.5" instead of "k2p5" id.includes("k2p5") ) return {} - // ToothFairyAI doesn't support thinking/reasoning parameters yet - if (model.api.npm === "@toothfairyai/sdk") return {} - // see: https://docs.x.ai/docs/guides/reasoning#control-how-hard-the-model-thinks if (id.includes("grok") && id.includes("grok-3-mini")) { if (model.api.npm === "@openrouter/ai-sdk-provider") {