Merge pull request #114 from modelstudioai/feat/token-plan-default-models

feat: update Token Plan defaults and support local image inputs
This commit is contained in:
Gong Shiqi
2026-07-22 10:52:35 +08:00
committed by GitHub
29 changed files with 711 additions and 82 deletions
+3
View File
@@ -33,6 +33,9 @@ export type AuthPersistPatch = Pick<
| "console_switch_agent"
| "workspace_id"
| "default_text_model"
| "default_video_model"
| "default_image_to_video_model"
| "default_reference_to_video_model"
| "default_image_model"
>;
+27 -1
View File
@@ -4,7 +4,7 @@ import { BailianError } from "../errors/base.ts";
import { ExitCode } from "../errors/codes.ts";
import { request, requestJson, type HttpDeps, type RequestOpts } from "./http.ts";
import { buildAcsCanonicalQuery, signAcsRequest, type AcsQueryParams } from "./acs.ts";
import { isLocalFile, resolveFileUrl } from "../files/upload.ts";
import { imageFileToDataUri, isLocalFile, resolveFileUrl } from "../files/upload.ts";
import { McpClient } from "./mcp.ts";
import { callConsoleGateway } from "../console/gateway.ts";
import { refreshAccessToken } from "../auth/refresh-token.ts";
@@ -118,6 +118,32 @@ export class Client {
return resolveFileUrl(source, this.requireApi().token, model, opts);
}
/**
* Resolve an image input while keeping Token Plan's upload limitation isolated.
* Token Plan local images are sent as Data URIs; every other connection keeps
* the established temporary OSS upload flow. URLs and existing Data URIs pass through.
*/
resolveImageInput(
source: string,
model: string,
opts: { signal?: AbortSignal } = {},
): Promise<string> {
if (!isLocalFile(source)) return Promise.resolve(source);
if (this.usesTokenPlanEndpoint()) {
return Promise.resolve(imageFileToDataUri(source));
}
return this.uploadFile(source, model, { signal: opts.signal });
}
usesTokenPlanEndpoint(): boolean {
if (this.deps.settings.configName === "token-plan") return true;
try {
return /^token-plan\.[a-z0-9-]+\.maas\.aliyuncs\.com$/i.test(new URL(this.baseUrl).hostname);
} catch {
return false;
}
}
/** Open an MCP client. Accepts a path (prepended with the model baseUrl) or an absolute URL. */
mcp(pathOrUrl: string): McpClient {
const url = /^https?:\/\//.test(pathOrUrl) ? pathOrUrl : this.requireApi().baseUrl + pathOrUrl;
+2
View File
@@ -235,6 +235,8 @@ export function buildSettings(s: ResolutionSources): Settings {
timeout,
defaultTextModel: file.default_text_model,
defaultVideoModel: file.default_video_model,
defaultImageToVideoModel: file.default_image_to_video_model,
defaultReferenceToVideoModel: file.default_reference_to_video_model,
defaultImageModel: file.default_image_model,
defaultSpeechModel: file.default_speech_model,
defaultOmniModel: file.default_omni_model,
+8 -2
View File
@@ -1,14 +1,20 @@
interface ModelProfilePreset {
baseUrl: string;
defaultTextModel: string;
defaultVideoModel: string;
defaultImageToVideoModel: string;
defaultReferenceToVideoModel: string;
defaultImageModel: string;
}
const MODEL_PROFILE_PRESETS: Readonly<Record<string, ModelProfilePreset>> = {
"token-plan": {
baseUrl: "https://token-plan.cn-beijing.maas.aliyuncs.com",
defaultTextModel: "qwen3.7-max",
defaultImageModel: "qwen-image-2.0",
defaultTextModel: "qwen3.8-max-preview",
defaultVideoModel: "happyhorse-1.1-t2v",
defaultImageToVideoModel: "happyhorse-1.1-i2v",
defaultReferenceToVideoModel: "happyhorse-1.1-r2v",
defaultImageModel: "wan2.7-image",
},
};
+16
View File
@@ -32,6 +32,8 @@ export interface ConfigFile {
timeout?: number;
default_text_model?: string;
default_video_model?: string;
default_image_to_video_model?: string;
default_reference_to_video_model?: string;
default_image_model?: string;
default_speech_model?: string;
default_omni_model?: string;
@@ -54,6 +56,8 @@ export const CONFIG_FILE_KEYS = [
"timeout",
"default_text_model",
"default_video_model",
"default_image_to_video_model",
"default_reference_to_video_model",
"default_image_model",
"default_speech_model",
"default_omni_model",
@@ -117,6 +121,16 @@ export function parseConfigFile(raw: unknown): ConfigFile {
out.default_text_model = obj.default_text_model;
if (typeof obj.default_video_model === "string" && obj.default_video_model.length > 0)
out.default_video_model = obj.default_video_model;
if (
typeof obj.default_image_to_video_model === "string" &&
obj.default_image_to_video_model.length > 0
)
out.default_image_to_video_model = obj.default_image_to_video_model;
if (
typeof obj.default_reference_to_video_model === "string" &&
obj.default_reference_to_video_model.length > 0
)
out.default_reference_to_video_model = obj.default_reference_to_video_model;
if (typeof obj.default_image_model === "string" && obj.default_image_model.length > 0)
out.default_image_model = obj.default_image_model;
if (typeof obj.default_speech_model === "string" && obj.default_speech_model.length > 0)
@@ -166,6 +180,8 @@ export interface Settings {
timeout: number;
defaultTextModel?: string;
defaultVideoModel?: string;
defaultImageToVideoModel?: string;
defaultReferenceToVideoModel?: string;
defaultImageModel?: string;
defaultSpeechModel?: string;
defaultOmniModel?: string;
+7 -1
View File
@@ -1 +1,7 @@
export { uploadFile, isLocalFile, resolveFileUrl } from "./upload.ts";
export {
uploadFile,
isLocalFile,
resolveFileUrl,
imageFileToDataUri,
redactDataUri,
} from "./upload.ts";
+44 -1
View File
@@ -6,7 +6,7 @@
* X-DashScope-OssResourceResolve: enable
*/
import { existsSync, readFileSync, statSync } from "fs";
import { basename } from "path";
import { basename, extname } from "path";
import { BailianError } from "../errors/base.ts";
import { ExitCode } from "../errors/codes.ts";
import { trackingHeaders } from "../client/headers.ts";
@@ -112,6 +112,49 @@ export interface UploadOptions {
signal?: AbortSignal;
}
const IMAGE_MIME_TYPES: Readonly<Record<string, string>> = {
".bmp": "image/bmp",
".heic": "image/heic",
".jpe": "image/jpeg",
".jpeg": "image/jpeg",
".jpg": "image/jpeg",
".png": "image/png",
".tif": "image/tiff",
".tiff": "image/tiff",
".webp": "image/webp",
};
/** Encode a local image as a Data URI. */
export function imageFileToDataUri(filePath: string): string {
if (!existsSync(filePath)) {
throw new BailianError(`File not found: ${filePath}`, ExitCode.USAGE);
}
const stat = statSync(filePath);
if (!stat.isFile()) {
throw new BailianError(`Not a file: ${filePath}`, ExitCode.USAGE);
}
const extension = extname(filePath).toLowerCase();
const mimeType = IMAGE_MIME_TYPES[extension];
if (!mimeType) {
throw new BailianError(
`Unsupported image format "${extension || "unknown"}".`,
ExitCode.USAGE,
"Use an image file with a recognized extension.",
);
}
const encoded = readFileSync(filePath).toString("base64");
return `data:${mimeType};base64,${encoded}`;
}
/** Keep dry-run output readable and avoid echoing the complete inline image. */
export function redactDataUri(input: string): string {
const match = /^data:([^;,]+);base64,/i.exec(input);
return match ? `data:${match[1]};base64,<omitted>` : input;
}
/**
* Upload a local file to DashScope temporary storage and return the oss:// URL.
* The URL is valid for 48 hours.
+15 -4
View File
@@ -32,8 +32,11 @@ const resolve = (s: Parameters<typeof src>[0]): Settings => buildSettings(src(s)
test("token-plan Profile 预设保持固定", () => {
expect(getModelProfilePreset("token-plan")).toEqual({
baseUrl: "https://token-plan.cn-beijing.maas.aliyuncs.com",
defaultTextModel: "qwen3.7-max",
defaultImageModel: "qwen-image-2.0",
defaultTextModel: "qwen3.8-max-preview",
defaultVideoModel: "happyhorse-1.1-t2v",
defaultImageToVideoModel: "happyhorse-1.1-i2v",
defaultReferenceToVideoModel: "happyhorse-1.1-r2v",
defaultImageModel: "wan2.7-image",
});
});
@@ -316,10 +319,18 @@ test("openapi 凭证:低优先级来源缺字段时不影响更高优先级成
test("default*Model / outputDir:仅 file 源", () => {
const c = resolve({
file: { default_text_model: "qwen-max", default_video_model: "wan-x", output_dir: "/tmp/out" },
file: parseConfigFile({
default_text_model: "qwen-max",
default_video_model: "wan-t2v",
default_image_to_video_model: "wan-i2v",
default_reference_to_video_model: "wan-r2v",
output_dir: "/tmp/out",
}),
});
expect(c.defaultTextModel).toBe("qwen-max");
expect(c.defaultVideoModel).toBe("wan-x");
expect(c.defaultVideoModel).toBe("wan-t2v");
expect(c.defaultImageToVideoModel).toBe("wan-i2v");
expect(c.defaultReferenceToVideoModel).toBe("wan-r2v");
expect(c.outputDir).toBe("/tmp/out");
expect(resolve({}).defaultTextModel).toBeUndefined();
});
+91
View File
@@ -0,0 +1,91 @@
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, test } from "vite-plus/test";
import { Client } from "../src/client/client.ts";
import { imageFileToDataUri, redactDataUri } from "../src/files/upload.ts";
import type { Settings } from "../src/config/schema.ts";
const tempDirs: string[] = [];
afterEach(() => {
for (const tempDir of tempDirs.splice(0)) {
rmSync(tempDir, { recursive: true, force: true });
}
});
function makeImage(extension = ".png", content = Buffer.from([1, 2, 3, 4])): string {
const tempDir = mkdtempSync(join(tmpdir(), "bailian-image-input-"));
tempDirs.push(tempDir);
const filePath = join(tempDir, `input${extension}`);
writeFileSync(filePath, content);
return filePath;
}
function makeSettings(configName?: string): Settings {
return {
configName,
output: "json",
outputExplicit: false,
timeout: 30,
verbose: false,
quiet: true,
dryRun: false,
telemetry: false,
};
}
function makeClient(baseUrl: string, configName?: string): Client {
return new Client({
identity: {
binName: "bl",
version: "test",
npmPackage: "bailian-cli",
clientName: "bailian-cli-test",
},
settings: makeSettings(configName),
baseUrl,
});
}
describe("Token Plan image input compatibility", () => {
test("encodes supported local images and redacts previews", () => {
const imagePath = makeImage(".png");
const dataUri = imageFileToDataUri(imagePath);
expect(dataUri).toBe("data:image/png;base64,AQIDBA==");
expect(redactDataUri(dataUri)).toBe("data:image/png;base64,<omitted>");
});
test("rejects files whose image MIME type cannot be inferred", () => {
const imagePath = makeImage(".unknown");
expect(() => imageFileToDataUri(imagePath)).toThrow(/Unsupported image format/);
});
test("uses Data URI for the token-plan profile even through a custom proxy", async () => {
const imagePath = makeImage(".webp");
const client = makeClient("https://proxy.example.com/bailian", "token-plan");
await expect(client.resolveImageInput(imagePath, "happyhorse-1.1-i2v")).resolves.toMatch(
/^data:image\/webp;base64,/,
);
});
test("uses Data URI for an official Token Plan endpoint under any profile name", async () => {
const imagePath = makeImage(".jpg");
const client = makeClient("https://token-plan.ap-southeast-1.maas.aliyuncs.com", "custom-plan");
await expect(client.resolveImageInput(imagePath, "wan2.7-image")).resolves.toMatch(
/^data:image\/jpeg;base64,/,
);
});
test("ordinary endpoints retain the existing upload path", () => {
const imagePath = makeImage(".png");
const client = makeClient("https://dashscope.aliyuncs.com", "default");
expect(() => client.resolveImageInput(imagePath, "wan2.7-image")).toThrow(
/model-domain API key/,
);
});
});