OpenClaw + GitHub Copilot GPT-5.4

One-page deployment guide for humans and coding agents.

This site renders the full guide as a static page and also exposes raw fetchable paths for agents that need plain text or Markdown.

OpenClaw GitHub Copilot GPT-5.4 Quick Deployment Guide

Audience: coding agents

Goal: complete a one-shot deployment that makes OpenClaw use GitHub Copilot GPT-5.4, including verification and failure handling.

Scope

This guide fixes three things in one pass:

1. Set the default model to github-copilot/gpt-5.4

2. Add the GitHub Copilot provider definition, IDE auth headers, and model entry

3. Force the current main session to use gpt-5.4

Prerequisite

If GitHub Copilot is not logged in yet, run:

openclaw models auth login-github-copilot

Optional status check:

openclaw models status --json

One-shot deployment

Run the full script below. It first probes whether Copilot really accepts gpt-5.4, then patches OpenClaw config, runtime model registry, and the main session override, and finally restarts the gateway.

set -euo pipefail

command -v openclaw >/dev/null || { echo 'missing: openclaw'; exit 1; }
command -v python3 >/dev/null || { echo 'missing: python3'; exit 1; }
command -v node >/dev/null || { echo 'missing: node'; exit 1; }

TOKEN_FILE="$HOME/.openclaw/credentials/github-copilot.token.json"
[ -f "$TOKEN_FILE" ] || {
	echo 'missing Copilot token: run openclaw models auth login-github-copilot'
	exit 1
}

export OPENCLAW_COPILOT_BASE_URL="$(node - <<'NODE'
const fs = require('fs');

const tokenPath = `${process.env.HOME}/.openclaw/credentials/github-copilot.token.json`;
const { token } = JSON.parse(fs.readFileSync(tokenPath, 'utf8'));
const match = token.match(/(?:^|;)\s*proxy-ep=([^;\s]+)/i);

const candidates = [
	match && match[1] ? `https://${String(match[1]).replace(/^proxy\./i, 'api.')}` : null,
	'https://api.enterprise.githubcopilot.com',
	'https://api.individual.githubcopilot.com',
].filter(Boolean);

const uniqueCandidates = [...new Set(candidates)];
const headers = {
	'Content-Type': 'application/json',
	Authorization: `Bearer ${token}`,
	'User-Agent': 'GitHubCopilotChat/0.35.0',
	'Editor-Version': 'vscode/1.107.0',
	'Editor-Plugin-Version': 'copilot-chat/0.35.0',
	'Copilot-Integration-Id': 'vscode-chat',
	'Openai-Intent': 'conversation-edits',
	'X-Initiator': 'user',
};

(async () => {
	for (const base of uniqueCandidates) {
		try {
			const res = await fetch(`${base}/responses`, {
				method: 'POST',
				headers,
				body: JSON.stringify({
					model: 'gpt-5.4',
					input: [{ role: 'user', content: [{ type: 'input_text', text: 'Reply with OK only' }] }],
					max_output_tokens: 16,
					stream: false,
					store: false,
				}),
			});
			const body = await res.text();
			if (res.ok) {
				console.error(`probe ok: ${base} status=${res.status}`);
				process.stdout.write(base);
				return;
			}
			console.error(`probe failed: ${base} status=${res.status} body=${body.slice(0, 160)}`);
		} catch (error) {
			console.error(`probe error: ${base} ${String(error)}`);
		}
	}
	process.exit(1);
})();
NODE
)"

echo "using baseUrl=$OPENCLAW_COPILOT_BASE_URL"

python3 - <<'PY'
import json
import os
import pathlib

home = pathlib.Path.home()
base_url = os.environ['OPENCLAW_COPILOT_BASE_URL']

provider_headers = {
		'User-Agent': 'GitHubCopilotChat/0.35.0',
		'Editor-Version': 'vscode/1.107.0',
		'Editor-Plugin-Version': 'copilot-chat/0.35.0',
		'Copilot-Integration-Id': 'vscode-chat',
		'Openai-Intent': 'conversation-edits',
		'X-Initiator': 'user',
}

model_def = {
		'id': 'gpt-5.4',
		'name': 'gpt-5.4',
		'api': 'openai-responses',
		'reasoning': True,
		'input': ['text', 'image'],
		'cost': {'input': 0, 'output': 0, 'cacheRead': 0, 'cacheWrite': 0},
		'contextWindow': 400000,
		'maxTokens': 128000,
}

def load_json(path):
		return json.loads(path.read_text()) if path.exists() else {}

def write_json(path, data):
		path.parent.mkdir(parents=True, exist_ok=True)
		path.write_text(json.dumps(data, ensure_ascii=True, indent=2) + '\n')

def upsert_model(model_list, model):
		for index, item in enumerate(model_list):
				if item.get('id') == model['id']:
						merged = dict(item)
						merged.update(model)
						model_list[index] = merged
						return
		model_list.append(model)

openclaw_config = home / '.openclaw' / 'openclaw.json'
if not openclaw_config.exists():
		raise SystemExit(f'missing config: {openclaw_config}')

config = load_json(openclaw_config)
agents = config.setdefault('agents', {})
defaults = agents.setdefault('defaults', {})
default_model = defaults.setdefault('model', {})
default_model['primary'] = 'github-copilot/gpt-5.4'
default_model['fallbacks'] = []
defaults.setdefault('models', {})['github-copilot/gpt-5.4'] = {}

providers = config.setdefault('models', {}).setdefault('providers', {})
provider = providers.setdefault('github-copilot', {})
provider['baseUrl'] = base_url
provider.setdefault('headers', {}).update(provider_headers)
provider_models = provider.setdefault('models', [])
upsert_model(provider_models, model_def)
write_json(openclaw_config, config)

runtime_models = home / '.openclaw' / 'agents' / 'main' / 'agent' / 'models.json'
runtime = load_json(runtime_models) or {'providers': {}}
runtime_provider = runtime.setdefault('providers', {}).setdefault('github-copilot', {})
runtime_provider['baseUrl'] = base_url
runtime_provider.setdefault('headers', {}).update(provider_headers)
runtime_provider_models = runtime_provider.setdefault('models', [])
upsert_model(runtime_provider_models, model_def)
write_json(runtime_models, runtime)

sessions_file = home / '.openclaw' / 'agents' / 'main' / 'sessions' / 'sessions.json'
if sessions_file.exists():
		sessions = load_json(sessions_file)
		session = sessions.get('agent:main:main')
		if isinstance(session, dict):
				session['modelOverride'] = 'gpt-5.4'
				session['model'] = 'gpt-5.4'
				session['modelProvider'] = 'github-copilot'
				session.pop('fallbackNoticeSelectedModel', None)
				session.pop('fallbackNoticeActiveModel', None)
				session.pop('fallbackNoticeReason', None)
				report = session.get('systemPromptReport')
				if isinstance(report, dict):
						report['model'] = 'gpt-5.4'
						report['provider'] = 'github-copilot'
				write_json(sessions_file, sessions)

print('patched openclaw.json, models.json and sessions.json')
PY

openclaw gateway restart
echo 'DEPLOY_OK'

Validation

1. Validate config state

python3 - <<'PY'
import json
import pathlib

home = pathlib.Path.home()

config = json.loads((home / '.openclaw' / 'openclaw.json').read_text())
runtime = json.loads((home / '.openclaw' / 'agents' / 'main' / 'agent' / 'models.json').read_text())

session_path = home / '.openclaw' / 'agents' / 'main' / 'sessions' / 'sessions.json'
session = {}
if session_path.exists():
		session = json.loads(session_path.read_text()).get('agent:main:main', {})

provider_models = runtime.get('providers', {}).get('github-copilot', {}).get('models', [])
has_gpt54 = any(item.get('id') == 'gpt-5.4' for item in provider_models)

print('primary =', config['agents']['defaults']['model']['primary'])
print('provider_base =', config['models']['providers']['github-copilot']['baseUrl'])
print('runtime_has_gpt54 =', has_gpt54)
print('session_modelOverride =', session.get('modelOverride'))
print('session_model =', session.get('model'))
PY

Expected:

2. Validate the real Copilot request

node - <<'NODE'
const fs = require('fs');
const tokenPath = `${process.env.HOME}/.openclaw/credentials/github-copilot.token.json`;
const { token } = JSON.parse(fs.readFileSync(tokenPath, 'utf8'));
const match = token.match(/(?:^|;)\s*proxy-ep=([^;\s]+)/i);
const base = match && match[1]
	? `https://${String(match[1]).replace(/^proxy\./i, 'api.')}`
	: 'https://api.enterprise.githubcopilot.com';

const headers = {
	'Content-Type': 'application/json',
	Authorization: `Bearer ${token}`,
	'User-Agent': 'GitHubCopilotChat/0.35.0',
	'Editor-Version': 'vscode/1.107.0',
	'Editor-Plugin-Version': 'copilot-chat/0.35.0',
	'Copilot-Integration-Id': 'vscode-chat',
	'Openai-Intent': 'conversation-edits',
	'X-Initiator': 'user',
};

(async () => {
	const res = await fetch(`${base}/responses`, {
		method: 'POST',
		headers,
		body: JSON.stringify({
			model: 'gpt-5.4',
			input: [{ role: 'user', content: [{ type: 'input_text', text: 'Reply with OK only' }] }],
			max_output_tokens: 16,
			stream: false,
			store: false,
		}),
	});
	const body = await res.text();
	console.log(JSON.stringify({ status: res.status, ok: res.ok, body: body.slice(0, 120) }, null, 2));
})();
NODE

Expected:

3. Validate the OpenClaw main session

If a main session already exists, run:

SESSION_ID="$(python3 - <<'PY'
import json
import pathlib

path = pathlib.Path.home() / '.openclaw' / 'agents' / 'main' / 'sessions' / 'sessions.json'
if path.exists():
		session = json.loads(path.read_text()).get('agent:main:main', {})
		print(session.get('sessionId', ''))
PY
)"

if [ -n "$SESSION_ID" ]; then
	openclaw agent --session-id "$SESSION_ID" --message "Reply with exactly OK" --json
else
	echo 'no main session yet: open OpenClaw once, then rerun this validation'
fi

Expected:

Failure handling

Unknown model: github-copilot/gpt-5.4

Cause: the runtime model registry was not patched.

Fix: rerun the full deployment script and confirm models.json contains gpt-5.4.

Missing Editor-Version or other IDE auth headers

Cause: the provider headers were not written.

Fix: rerun the full deployment script and confirm both openclaw.json and models.json contain github-copilot.headers.

TUI still shows the old model

Cause: the main session still has the old modelOverride, or the client has not reconnected.

Fix:

1. Rerun the full deployment script

2. Run openclaw gateway restart

3. Fully reconnect the TUI or client

The initial probe request fails

Cause: this is usually not an OpenClaw config issue. It is usually a Copilot login, token, or entitlement issue.

Fix:

1. Run openclaw models auth login-github-copilot again

2. Rerun the deployment script

Short execution summary for future agents

1. Log into GitHub Copilot

2. Run the one-shot deployment script

3. Run the validation steps

4. If the TUI still shows the old model, reconnect the client

Do not patch only openclaw.json. Older OpenClaw builds often also require models.json and the current main session to be updated.