npx skills add ...
npx skills add tencentedgeone/edgeone-makers-tools --skill makers-migration
Migrate existing AI agent projects (LangChain, LangGraph, OpenAI Agents SDK, Claude Agent SDK, CrewAI) to EdgeOne Makers platform conventions. Use when the user wants to adapt a standard agent project to run on EdgeOne Makers, convert Express/Next.js API routes to Makers handlers, or add platform capabilities (context.tools, context.sandbox, context.store). Do NOT trigger for new agent projects (use makers-agents instead).
npx skills add tencentedgeone/edgeone-makers-tools --skill makers-migration
Migrate existing AI agent projects to the EdgeOne Makers platform format. Covers structural conversion, API adaptation, and platform capability injection.
Before starting framework-specific changes, check these global items:
edgeone.json with correct agents.framework and buildCommand/outputDirectoryagents/ directoryprocess.env / os.environ with context.env / ctx.envreq.headers.get('x') with context.request.headers['x'] (Node) or plain dict access (Python)await req.json() with context.request.body (already parsed)AI_GATEWAY_* env varsres.json() / return {"data": ...})makers-conversation-id header to frontend fetch callscontext.tools instead of custom tool implementationscontext.store instead of in-memory or custom DBWSA_API_KEY env var and use context.tools.get("web_search")edgeone makers dev for local developmentThis is the most common migration pattern. Applies to Express/Next.js API routes, plain HTTP handlers, etc.
| Before | After |
|---|---|
os.environ.get("OPENAI_API_KEY") | ctx.env.get("AI_GATEWAY_API_KEY") |
LLM(provider="openai", ...) — LiteLLM dispatch | LLM(provider="openai", base_url=ctx.env["AI_GATEWAY_BASE_URL"], ...) — bypass LiteLLM |
memory=True on Crew | memory=False + use ctx.store |
verbose=True | verbose=False (events go through crewai_event_bus) |
crew.kickoff() (blocking) | await asyncio.to_thread(crew.kickoff) |
| Custom search tools | Use ctx.tools.to_crewai_tools(BaseTool) |
| Flask/FastAPI handler | async def handler(ctx): → ctx.utils.stream_sse(gen()) |
async def handler(ctx):ctx.env, never os.environLLM(provider="openai", api_key=ctx.env["AI_GATEWAY_API_KEY"], base_url=ctx.env["AI_GATEWAY_BASE_URL"])Crew(memory=False, verbose=False)crew.kickoff() in asyncio.to_thread()ctx.tools.to_crewai_tools(BaseTool)ctx.utils.stream_sse(gen())See makers-agents/references/python-frameworks/crewai.md for the complete pattern. Detailed before/after: references/crewai-to-makers.md
| Before | After |
|---|---|
Direct model creation (new ChatOpenAI(...)) | Use AI_GATEWAY_* for apiKey/baseURL |
MemorySaver (in-memory checkpointer) | context.store.langgraphCheckpointer (persistent) |
| Custom tool functions | context.tools.toLangChainTools(tool) |
agent.stream() | SSE via createSSEResponse(gen, signal) (Node) or ctx.utils.stream_sse(gen()) (Python) |
thread_id manual management | thread_id = context.conversation_id |
agents/<name>/index.ts (or .py)AI_GATEWAY_API_KEY + AI_GATEWAY_BASE_URLcontext.store.langgraphCheckpointer instead of MemorySavercontext.store.langgraphStorecontext.tools.toLangChainTools(tool) instead of custom tool functionsthread_id: { configurable: { thread_id: context.conversation_id } }Node: makers-agents/references/node-frameworks/langgraph.md Python: makers-agents/references/python-frameworks/langgraph.md DeepAgents: makers-agents/references/node-frameworks/deepagents.md Detailed before/after: references/langgraph-to-makers.md, references/deepagents-to-makers.md
| Before | After |
|---|---|
new OpenAI({ apiKey, baseURL }) | Read AI_GATEWAY_* from context.env / ctx.env |
Runner.run(agent, input, { tools }) | Tools from context.tools.all() (already OpenAI function format) |
| Session management | context.store.openaiSession(convId) (Node) |
| Express route response | SSE via createSSEResponse(gen, signal) (Node) or ctx.utils.stream_sse(gen()) (Python) |
| Model name hardcoded | `ctx.env.AI_GATEWAY_MODEL |
agents/<name>/index.ts (or .py)context.env (not process.env)context.tools.all() (returns OpenAI function tools)context.store.openaiSession(conversationId) for session (Node)output_text_delta → ai_response, tool_called → tool_callNode: makers-agents/references/node-frameworks/openai-agents.md Python: makers-agents/references/python-frameworks/openai-agents.md Detailed before/after: references/openai-agents-to-makers.md
| Before | After |
|---|---|
ANTHROPIC_API_KEY env var | Mapped from AI_GATEWAY_* via collectGatewayEnv() |
process.env | context.env injected into query().options.env |
| Custom MCP tools | context.tools.toClaudeMcpServer() |
| Session | context.store.claudeSessionStore() (Node) |
| Stdout EPIPE crash | Swallow EPIPE on process.stdout (Node) |
| No writable config dir | Set CLAUDE_CONFIG_DIR=/tmp/claude-agent-sdk, CLAUDE_CODE_TMPDIR=/tmp |
agents/<name>/index.ts (or .py)AI_GATEWAY_* → ANTHROPIC_* via collectGatewayEnv(context.env)query({ options: { env: collectGatewayEnv(...) } })context.tools.toClaudeMcpServer('edgeone', { alwaysLoad: true })EPIPE on process.stdoutCLAUDE_CONFIG_DIR=/tmp/claude-agent-sdk, CLAUDE_CODE_TMPDIR=/tmpNode: makers-agents/references/node-frameworks/claude-sdk.md Python: makers-agents/references/python-frameworks/claude-sdk.md Detailed before/after: references/claude-agent-sdk-to-makers.md
General migration for any Express-based or Next.js API route agent.
| Step | Before | After |
|---|---|---|
| 1. File location | app/api/chat/route.ts or server/routes/chat.ts | agents/chat/index.ts |
| 2. Entry signature | export async function POST(req) or app.post('/chat', handler) | export async function onRequest(context) |
| 3. Body parsing | await req.json() | context.request.body (already parsed) |
| 4. Headers | req.headers.get('x-foo') | context.request.headers['x-foo'] |
| 5. Abort signal | req.signal | context.request.signal (AbortSignal) |
| 6. Model access | process.env.OPENAI_API_KEY → direct call | context.env.AI_GATEWAY_* → AI Gateway |
| 7. Response | res.json() or return Response.json() | SSE stream via createSSEResponse(gen, signal) |
After migration, verify these items before deploying:
edgeone makers dev starts without errors/chat endpoint returns SSE stream (not JSON)context.env is used everywhere (grep for process.env / os.environ — none should remain)edgeone.json has correct agents.frameworkcontext.tools) work in at least one frameworkcontext.store)/stop endpoint cancels active runsmakers-conversation-id headeronRequest/handler)// ❌ Before: Next.js API route (app/api/chat/route.ts)
export async function POST(req: Request) {
const body = await req.json();
const headers = req.headers;
const apiKey = process.env.OPENAI_API_KEY;
// ... LLM call ...
return Response.json({ data: result });
}
// ✅ After: Makers agent handler (agents/chat/index.ts)
export async function onRequest(context: any) {
const body = context.request.body; // already parsed
const conversationId = context.conversation_id; // auto-injected from header
const env = context.env; // context.env, never process.env
// ... LLM call via AI_GATEWAY_* ...
return new Response(JSON.stringify({ data: result }), {
headers: { 'Content-Type': 'application/json' },
});
}# ❌ Before: Flask route
@app.route('/chat', methods=['POST'])
def chat():
body = request.get_json()
api_key = os.environ.get('OPENAI_API_KEY')
# ... LLM call ...
return jsonify({'data': result})
# ✅ After: Makers agent handler (agents/chat/index.py)
async def handler(ctx):
body = ctx.request.body
conversation_id = ctx.conversation_id
api_key = ctx.env.get("AI_GATEWAY_API_KEY")
# ... LLM call via AI_GATEWAY_* ...
return {"data": result}{
"buildCommand": "",
"outputDirectory": "",
"agents": {
"framework": "crewai"
}
}crewai>=1.14.5
openai>=1.50.0{
"agents": {
"framework": "langgraph"
}
}{
"buildCommand": "",
"outputDirectory": "",
"agents": {
"framework": "langgraph"
}
}{
"agents": {
"framework": "openai-agents-sdk"
}
}{
"buildCommand": "",
"outputDirectory": "",
"agents": {
"framework": "openai-agents-sdk"
}
}{
"agents": {
"framework": "claude-agent-sdk"
}
}{
"buildCommand": "",
"outputDirectory": "",
"agents": {
"framework": "claude-agent-sdk"
}
}// ❌ Before: Next.js (app/api/chat/route.ts)
import { NextRequest } from 'next/server';
import OpenAI from 'openai';
export async function POST(req: NextRequest) {
const { message } = await req.json();
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const response = await client.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: message }],
stream: true,
});
// ... stream back as Response
}
// ✅ After: EdgeOne Makers (agents/chat/index.ts)
import { createLogger, sseEvent, createSSEResponse } from '../_shared';
export async function onRequest(context: any) {
const { message } = context.request.body ?? {};
if (!message) return new Response('Missing message', { status: 400 });
const signal = context.request.signal as AbortSignal;
return createSSEResponse(async function* (sig) {
const response = await fetch(context.env.AI_GATEWAY_BASE_URL + '/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${context.env.AI_GATEWAY_API_KEY}`,
},
body: JSON.stringify({
model: context.env.AI_GATEWAY_MODEL || '@makers/deepseek-v4-flash',
messages: [{ role: 'user', content: message }],
stream: true,
}),
signal: sig,
});
// ... proxy SSE chunks ...
yield 'data: [DONE]\n\n';
}, signal);
}// ❌ Before: plain fetch without conversation-id
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message }),
});
// ✅ After: with makers-conversation-id header
const conversationId = getOrCreateConversationId(); // crypto.randomUUID() + localStorage
const response = await fetch('/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'makers-conversation-id': conversationId, // ⭐ required for all AI endpoints
},
body: JSON.stringify({ message }),
});// ✅ Always pass conversation_id in body for /stop
await fetch('/stop', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ conversation_id: conversationId }),
});const reader = response.body!.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') return;
try {
const event = JSON.parse(data);
if (event.type === 'ai_response') { /* display text */ }
if (event.type === 'tool_call') { /* show tool call */ }
if (event.type === 'ping') { /* ignore heartbeat */ }
} catch { /* skip non-JSON */ }
}
}
}