fix: use Obsidian requestUrl instead of fetch (CSP bypass), add model dropdown

native fetch blocked by Obsidian CSP for external domains; requestUrl
bypasses it. Also adds Codex model dropdown with known models + custom
fallback, updates normalizeModel for gpt-5.5/5.4-mini/5.3-codex-spark.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Felix Isaac Lim 2026-05-23 06:18:39 +08:00
parent 6880043c8b
commit b389c5991d
3 changed files with 79 additions and 25 deletions

View file

@ -1,5 +1,5 @@
import * as http from "http";
import { Notice } from "obsidian";
import { Notice, requestUrl } from "obsidian";
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
@ -58,7 +58,8 @@ function extractAccountId(accessToken: string): string | null {
}
async function exchangeCode(code: string, verifier: string): Promise<CodexTokens | null> {
const res = await fetch(TOKEN_URL, {
const res = await requestUrl({
url: TOKEN_URL,
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
@ -67,12 +68,13 @@ async function exchangeCode(code: string, verifier: string): Promise<CodexTokens
code,
code_verifier: verifier,
redirect_uri: REDIRECT_URI,
}),
}).toString(),
throw: false,
});
if (!res.ok) return null;
if (res.status < 200 || res.status >= 300) return null;
const json = await res.json() as any;
const json = res.json as any;
if (!json.access_token || !json.refresh_token) return null;
const accountId = extractAccountId(json.access_token);
@ -87,19 +89,21 @@ async function exchangeCode(code: string, verifier: string): Promise<CodexTokens
}
export async function refreshCodexToken(tokens: CodexTokens): Promise<CodexTokens | null> {
const res = await fetch(TOKEN_URL, {
const res = await requestUrl({
url: TOKEN_URL,
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: tokens.refresh,
client_id: CLIENT_ID,
}),
}).toString(),
throw: false,
});
if (!res.ok) return null;
if (res.status < 200 || res.status >= 300) return null;
const json = await res.json() as any;
const json = res.json as any;
if (!json.access_token || !json.refresh_token) return null;
return {

View file

@ -1,3 +1,5 @@
import { requestUrl } from "obsidian";
const CODEX_API_URL = "https://chatgpt.com/backend-api/codex/responses";
interface ResponsesInput {
@ -19,13 +21,16 @@ interface ResponsesBody {
function normalizeModel(model: string): string {
const m = model.toLowerCase().trim();
if (m === "gpt-5.5" || m.includes("gpt-5.5")) return "gpt-5.5";
if (m === "gpt-5.4-mini" || m.includes("gpt-5.4-mini")) return "gpt-5.4-mini";
if (m.includes("gpt-5.3-codex-spark") || m.includes("codex-spark")) return "gpt-5.3-codex-spark";
if (m.includes("gpt-5.2-codex") || m.includes("gpt 5.2 codex")) return "gpt-5.2-codex";
if (m.includes("gpt-5.1-codex-max") || m.includes("codex-max")) return "gpt-5.1-codex-max";
if (m.includes("codex-mini-latest") || m.includes("codex-mini")) return "codex-mini-latest";
if (m.includes("gpt-5.1-codex") || m.includes("codex")) return "gpt-5.1-codex";
if (m.includes("gpt-5.2")) return "gpt-5.2";
if (m.includes("gpt-5.1")) return "gpt-5.1";
return "gpt-5.1-codex";
return m; // pass through unknown models as-is
}
function parseSseText(sseBody: string): string {
@ -102,7 +107,8 @@ export async function callCodexApi(
include: ["reasoning.encrypted_content"],
};
const res = await fetch(CODEX_API_URL, {
const res = await requestUrl({
url: CODEX_API_URL,
method: "POST",
headers: {
"Content-Type": "application/json",
@ -113,14 +119,14 @@ export async function callCodexApi(
"accept": "text/event-stream",
},
body: JSON.stringify(body),
throw: false,
});
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`Codex API ${res.status}: ${text.slice(0, 200)}`);
if (res.status < 200 || res.status >= 300) {
throw new Error(`Codex API ${res.status}: ${res.text.slice(0, 200)}`);
}
const rawText = await res.text();
const rawText = res.text;
const result = parseSseText(rawText);
if (!result) throw new Error("Codex returned empty response");
return result;

View file

@ -128,17 +128,61 @@ export class InlineAISettingsTab extends PluginSettingTab {
}
// Model setting
new Setting(containerEl)
.setName("Model")
.setDesc("Specify the model to use.")
.addText((text) => {
text.setPlaceholder("e.g., gpt-4o-mini")
.setValue(this.plugin.settings.model)
.inputEl.addEventListener("blur", async () => {
this.plugin.settings.model = text.getValue();
await this.saveSettings();
if (this.plugin.settings.provider === "codex") {
const CODEX_MODELS = [
{ value: "gpt-5.5", label: "GPT-5.5 (recommended)" },
{ value: "gpt-5.4-mini", label: "GPT-5.4 mini (faster)" },
{ value: "gpt-5.3-codex-spark", label: "GPT-5.3 Codex Spark (Pro only)" },
{ value: "gpt-5.2-codex", label: "GPT-5.2 Codex" },
{ value: "gpt-5.1-codex", label: "GPT-5.1 Codex" },
{ value: "gpt-5.1-codex-max", label: "GPT-5.1 Codex Max" },
{ value: "codex-mini-latest", label: "Codex Mini" },
{ value: "custom", label: "Custom…" },
];
const isCustom = !CODEX_MODELS.some(
(m) => m.value === this.plugin.settings.model && m.value !== "custom",
);
const dropdownValue = isCustom ? "custom" : this.plugin.settings.model;
new Setting(containerEl)
.setName("Model")
.setDesc("Select a Codex model.")
.addDropdown((dd) => {
CODEX_MODELS.forEach((m) => dd.addOption(m.value, m.label));
dd.setValue(dropdownValue).onChange(async (value) => {
if (value !== "custom") {
this.plugin.settings.model = value;
await this.saveSettings();
}
this.display();
});
});
});
if (isCustom || dropdownValue === "custom") {
new Setting(containerEl)
.setName("Custom model ID")
.addText((text) => {
text.setPlaceholder("e.g., gpt-5.1-codex")
.setValue(isCustom ? this.plugin.settings.model : "")
.inputEl.addEventListener("blur", async () => {
this.plugin.settings.model = text.getValue();
await this.saveSettings();
});
});
}
} else {
new Setting(containerEl)
.setName("Model")
.setDesc("Specify the model to use.")
.addText((text) => {
text.setPlaceholder("e.g., gpt-4o-mini")
.setValue(this.plugin.settings.model)
.inputEl.addEventListener("blur", async () => {
this.plugin.settings.model = text.getValue();
await this.saveSettings();
});
});
}
// API Key setting (conditionally displayed for OpenAI-supported endpoints)
if (