Last reviewed: 2026-08-03

Direct answer

Keep LangGraph responsible for orchestration and swap only the chat model adapter. Configure ChatOpenAI with CometAPI’s base URL, a current CometAPI model that supports Chat Completions and tool calling, and a deployment-managed credential. Models that require the Responses API are outside this graph contract. The CometAPI quick-start documents the OpenAI-compatible setup and distinguishes Chat Completions from Responses, while the ChatOpenAI integration documents the configuration.baseURL option, its standard process credential setting, and tool calling for compatible providers.

The graph itself follows the same shape as the LangGraph TypeScript quickstart : define a message state, call the model in one node, execute requested tools in another node, route on tool_calls, and compile the graph. This keeps provider configuration at the edge of the application instead of scattering CometAPI details through every node.

Install the packages in a server-side project:

npm install @langchain/langgraph @langchain/openai @langchain/core zod

Fail fast when the endpoint, credential, and model settings are absent, then create the model. The preflight check reads and validates COMETAPI_KEY at runtime. LangChain’s documented default process credential is OPENAI_API_KEY; configure deployment so that the same CometAPI secret is exposed through that slot as well, while COMETAPI_KEY remains the operator-facing preflight name. The source code never copies or prints the value.

import { ChatOpenAI } from '@langchain/openai';
import { tool } from '@langchain/core/tools';
import { AIMessage, HumanMessage, SystemMessage, ToolMessage } from '@langchain/core/messages';
import {
  END,
  START,
  StateGraph,
  StateSchema,
  MessagesValue,
  GraphNode,
  ConditionalEdgeRouter,
} from '@langchain/langgraph';
import * as z from 'zod';

const baseURL = process.env.COMETAPI_BASE_URL;
const modelId = process.env.COMETAPI_MODEL;

if (!baseURL) throw new Error('COMETAPI_BASE_URL is required');
if (!modelId) throw new Error('COMETAPI_MODEL is required');

if (!process.env.COMETAPI_KEY) throw new Error('COMETAPI_KEY is required');
if (!process.env.OPENAI_API_KEY) throw new Error('ChatOpenAI process credential is required');

const model = new ChatOpenAI({
  model: modelId,
  temperature: 0,
  configuration: { baseURL },
});

Here is a small, deterministic tool and graph. In a real service, replace the fixture lookup with a bounded database or HTTP operation. Keep the tool schema narrow so the model has fewer ambiguous choices.

const lookupOrder = tool(
  async ({ orderId }) => ({ orderId, status: 'processing' }),
  {
    name: 'lookup_order',
    description: 'Return the current status for an order identifier.',
    schema: z.object({
      orderId: z.string().regex(/^[A-Z0-9-]+$/),
    }),
  },
);

const toolsByName = { lookup_order: lookupOrder };
const modelWithTools = model.bindTools(Object.values(toolsByName));
const MessagesState = new StateSchema({ messages: MessagesValue });

const callModel: GraphNode<typeof MessagesState> = async (state) => {
  const response = await modelWithTools.invoke([
    new SystemMessage('Use lookup_order when the user asks for an order status.'),
    ...state.messages,
  ]);
  return { messages: [response] };
};

const callTools: GraphNode<typeof MessagesState> = async (state) => {
  const lastMessage = state.messages.at(-1);
  if (lastMessage == null || !AIMessage.isInstance(lastMessage)) {
    return { messages: [] };
  }

  const results: ToolMessage[] = [];
  for (const toolCall of lastMessage.tool_calls ?? []) {
    const selectedTool = toolsByName[toolCall.name as keyof typeof toolsByName];
    if (!selectedTool) {
      results.push(new ToolMessage({
        content: 'Unknown tool requested.',
        tool_call_id: toolCall.id ?? 'missing-id',
      }));
      continue;
    }
    results.push(await selectedTool.invoke(toolCall));
  }
  return { messages: results };
};

const routeAfterModel: ConditionalEdgeRouter<typeof MessagesState, 'tools'> = (state) => {
  const lastMessage = state.messages.at(-1);
  if (AIMessage.isInstance(lastMessage) && lastMessage.tool_calls?.length) {
    return 'tools';
  }
  return END;
};

const agent = new StateGraph(MessagesState)
  .addNode('model', callModel)
  .addNode('tools', callTools)
  .addEdge(START, 'model')
  .addConditionalEdges('model', routeAfterModel, ['tools', END])
  .addEdge('tools', 'model')
  .compile();

const result = await agent.invoke({
  messages: [new HumanMessage('Where is order ABC-123?')],
});
console.log(result.messages.at(-1)?.content);

Who this is for

This pattern is for a TypeScript developer who already understands basic LangGraph nodes and wants to use CometAPI as the model endpoint without rewriting the graph. It is also useful for an operator moving a prototype from a direct provider client to one OpenAI-compatible gateway. You should be comfortable with Node.js environment variables, async functions, and a small Zod schema.

It is not a complete persistence or deployment architecture. The LangGraph.js repository describes LangGraph as a low-level orchestration framework for stateful agents, so you still choose how to checkpoint state, authorize tools, and expose the agent to callers. The example keeps those concerns explicit rather than hiding them behind a high-level agent factory.

Key takeaways

  • Configure CometAPI once at the ChatOpenAI boundary. The graph nodes should consume a model object, not construct provider clients repeatedly.
  • Keep COMETAPI_KEY server-side. The CometAPI documentation explicitly recommends an environment variable or local environment file and warns against public repositories, screenshots, and frontend code.
  • Choose a current model that is listed for Chat Completions and supports tool calling; a Responses-only model is not interchangeable with this graph.
  • Bind only the tools the graph can actually execute. A model response is a request to run a tool, not permission to run arbitrary code.
  • Route only when the last message is an AIMessage with tool calls. Otherwise, return the answer and stop the graph.
  • Validate tool arguments before side effects and return a bounded ToolMessage for an unknown tool.
  • For a separate request-body review, see type a CometAPI chat request in TypeScript .

Sources checked

These are the public references used for the integration contract and graph shape:

The sources establish the adapter and orchestration patterns. They do not guarantee that every CometAPI model supports every LangChain feature, so model-specific checks remain part of the integration work.

Contract details to verify

Start with the values that affect the wire contract. Use the base URL documented by CometAPI, keep it in COMETAPI_BASE_URL, and select a currently listed model that is documented for Chat Completions and tool calling. CometAPI also documents models that require the Responses API; do not place one of those IDs in this ChatOpenAI tool-calling graph. The model ID is data, so making it an environment setting lets an operator change compatible models without editing graph code. Keep the CometAPI secret in COMETAPI_KEY and expose the same deployment-managed value through OPENAI_API_KEY for ChatOpenAI; do not pass it through a browser request or include it in logs.

The LangChain integration uses configuration.baseURL for a custom provider. It also supports custom headers and tool binding. Supply authentication through the deployment’s normal process-level credential integration so the graph never assembles an authorization header. If your deployment has a wrapper that does not support streaming usage metadata, the LangChain guide documents streamUsage: false as the compatibility setting. Apply that option only after observing the provider error; do not disable features preemptively.

The graph contract is equally concrete. MessagesValue stores the conversation state. The model node receives the system instruction plus prior messages and returns one model message. The router checks the last message for tool calls. The tool node validates each call against the Zod schema, executes only a named function from toolsByName, and appends tool results. The edge back to the model gives it a chance to turn the result into a final response. A model response without tool calls ends the run.

Keep side effects idempotent where possible. For a read-only lookup, a retry is usually safe. For a write operation, add an application-level idempotency key and an explicit confirmation step before connecting it to the graph. Before publishing a tutorial, normalize CometAPI text payloads for a reusable client so response extraction stays separate from graph routing.

Use a small operator test before adding real data. The happy path is a prompt such as the order-status request in the example: a Chat Completions model with tool support emits one lookup_order call, the tool returns a fixture, and the second model turn produces a user-readable status. The error path is deliberate: remove one environment variable, choose an invalid or Responses-only model ID, or alter the tool name. The process should fail before a network call for missing configuration, surface a provider or API-family error for an incompatible model, and return a controlled tool message for an unknown name.

Log only fields that help an operator reconstruct that sequence:

console.info({
  event: 'agent_run',
  graph_run_id: 'run-001',
  model: '[MODEL_ID]',
  tool_name: 'lookup_order',
  tool_call_count: 1,
  latency_ms: 842,
  outcome: 'success',
});

Do not log the API key, authorization data, full prompts, full message history, or unredacted tool arguments. If an order identifier is sensitive in your domain, hash it or record only a stable internal reference.

Failure modes

Missing configuration. A blank COMETAPI_KEY, OPENAI_API_KEY, COMETAPI_BASE_URL, or COMETAPI_MODEL should stop startup with a named configuration error. This is easier to fix than a later, ambiguous provider failure. If one credential variable is present and the other is absent, repair the deployment mapping instead of copying a value into source.

Wrong endpoint family. A base URL copied from a dashboard page, a browser URL, or a provider-specific SDK can produce an endpoint or authentication error. Compare the configured value with the CometAPI quick-start, then make one direct model call before debugging LangGraph.

Stale, Responses-only, or tool-incompatible model. CometAPI tells readers to choose a model by API family, and its quick-start distinguishes Chat Completions from Responses. Treat a model error or missing tool calls as a catalog/capability issue first. Confirm that the selected current model supports Chat Completions and tool calling; do not silently substitute a different model in the graph because tool behavior and response quality may change.

Streaming option mismatch. Some OpenAI-compatible providers do not accept the newer streaming-usage parameter. If the error names that option, set streamUsage: false in ChatOpenAI and retry the smallest request. Record the option change in configuration so it is visible to the next operator.

Malformed or unsafe tool input. Zod should reject an invalid identifier before the tool touches a database or external service. Unknown tool names should become a bounded ToolMessage, not a dynamic import or shell command. Add an execution limit so a graph cannot loop indefinitely on repeated tool calls.

Secrets in diagnostics. LangGraph state contains messages, and tool arguments can contain user data. Treat state snapshots and traces as sensitive operational data. Keep them out of normal logs, redact before exporting, and never use a real key in a copied example.

FAQ

Do I need a separate CometAPI SDK? No. The CometAPI quick-start documents OpenAI-compatible formats, and the LangChain integration documents a custom base URL. The graph can use ChatOpenAI while CometAPI supplies the model endpoint.

Does connecting CometAPI change the LangGraph graph? The model adapter changes, but the orchestration pattern does not. State, model node, tool node, conditional edge, compile step, and invoke step remain the same pattern shown in the LangGraph quickstart.

Which model should the example use? Use a current model ID from CometAPI’s catalog that is listed for Chat Completions and supports tool calling, then place it in COMETAPI_MODEL. A model listed for the Responses API is not a drop-in choice for this graph. Avoid inventing a model name in published code because availability and capabilities are model-specific.

How do I know whether a tool call happened? Inspect the last AIMessage and its tool_calls collection. The tool node should emit a ToolMessage, after which the graph returns to the model node. A final assistant message without tool calls is the normal stop condition.

What if CometAPI returns a compatibility error? Reduce the request to one model invocation, verify the base URL and model ID, then compare the failing option with the ChatOpenAI integration guide. A streaming-usage error is the case where streamUsage: false is documented as a targeted adjustment.

Where should the key live? Keep it in a server-side environment variable or a secret manager. Do not send it from a browser, commit it, paste it into screenshots, or include it in a support ticket.

Reader next step

Create a small server-side Node.js project, install the four packages above, provision the same CometAPI secret as COMETAPI_KEY and the ChatOpenAI process credential OPENAI_API_KEY, set the base URL and a Chat Completions/tool-capable model through deployment configuration, and run the fixture tool before connecting real data. Confirm one successful model-to-tool-to-model loop and one controlled Responses-only or invalid-tool test. Then review the CometAPI quick-start beside your model configuration and start with Start with CometAPI .