Skip to content

Commit c77a8a5

Browse files
authored
fix(openai): Prevent extra constructor params from being serialized, add script (#7669)
1 parent 1c1e6cd commit c77a8a5

9 files changed

+167
-2
lines changed

libs/langchain-openai/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"zod-to-json-schema": "^3.22.3"
4242
},
4343
"peerDependencies": {
44-
"@langchain/core": ">=0.3.29 <0.4.0"
44+
"@langchain/core": ">=0.3.39 <0.4.0"
4545
},
4646
"devDependencies": {
4747
"@azure/identity": "^4.2.1",

libs/langchain-openai/src/azure/chat_models.ts

+15
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,21 @@ export class AzureChatOpenAI extends ChatOpenAI {
473473
};
474474
}
475475

476+
get lc_serializable_keys(): string[] {
477+
return [
478+
...super.lc_serializable_keys,
479+
"azureOpenAIApiKey",
480+
"azureOpenAIApiVersion",
481+
"azureOpenAIBasePath",
482+
"azureOpenAIEndpoint",
483+
"azureOpenAIApiInstanceName",
484+
"azureOpenAIApiDeploymentName",
485+
"deploymentName",
486+
"openAIApiKey",
487+
"openAIApiVersion",
488+
];
489+
}
490+
476491
constructor(
477492
fields?: Partial<OpenAIChatInput> &
478493
Partial<AzureOpenAIInput> & {

libs/langchain-openai/src/chat_models.ts

+39
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,45 @@ export class ChatOpenAI<
905905
};
906906
}
907907

908+
get lc_serializable_keys(): string[] {
909+
return [
910+
"configuration",
911+
"logprobs",
912+
"topLogprobs",
913+
"prefixMessages",
914+
"supportsStrictToolCalling",
915+
"modalities",
916+
"audio",
917+
"reasoningEffort",
918+
"temperature",
919+
"maxTokens",
920+
"topP",
921+
"frequencyPenalty",
922+
"presencePenalty",
923+
"n",
924+
"logitBias",
925+
"user",
926+
"streaming",
927+
"streamUsage",
928+
"modelName",
929+
"model",
930+
"modelKwargs",
931+
"stop",
932+
"stopSequences",
933+
"timeout",
934+
"openAIApiKey",
935+
"apiKey",
936+
"cache",
937+
"maxConcurrency",
938+
"maxRetries",
939+
"verbose",
940+
"callbacks",
941+
"tags",
942+
"metadata",
943+
"disableStreaming",
944+
];
945+
}
946+
908947
temperature?: number;
909948

910949
topP?: number;

libs/langchain-openai/src/tests/azure/chat_models.test.ts

+14
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@ test("Test Azure OpenAI serialization from azure endpoint", async () => {
2323
);
2424
});
2525

26+
test("Test Azure OpenAI serialization does not pass along extra params", async () => {
27+
const chat = new AzureChatOpenAI({
28+
azureOpenAIEndpoint: "https://foobar.openai.azure.com/",
29+
azureOpenAIApiDeploymentName: "gpt-4o",
30+
azureOpenAIApiVersion: "2024-08-01-preview",
31+
azureOpenAIApiKey: "foo",
32+
extraParam: "extra",
33+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
34+
} as any);
35+
expect(JSON.stringify(chat)).toEqual(
36+
`{"lc":1,"type":"constructor","id":["langchain","chat_models","azure_openai","AzureChatOpenAI"],"kwargs":{"azure_endpoint":"https://foobar.openai.azure.com/","deployment_name":"gpt-4o","openai_api_version":"2024-08-01-preview","azure_open_ai_api_key":{"lc":1,"type":"secret","id":["AZURE_OPENAI_API_KEY"]}}}`
37+
);
38+
});
39+
2640
test("Test Azure OpenAI serialization from base path", async () => {
2741
const chat = new AzureChatOpenAI({
2842
azureOpenAIBasePath:

libs/langchain-openai/src/tests/chat_models.test.ts

+12
Original file line numberDiff line numberDiff line change
@@ -259,3 +259,15 @@ describe("strict tool calling", () => {
259259
}
260260
});
261261
});
262+
263+
test("Test OpenAI serialization doesn't pass along extra params", async () => {
264+
const chat = new ChatOpenAI({
265+
apiKey: "test-key",
266+
model: "o3-mini",
267+
somethingUnexpected: true,
268+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
269+
} as any);
270+
expect(JSON.stringify(chat)).toEqual(
271+
`{"lc":1,"type":"constructor","id":["langchain","chat_models","openai","ChatOpenAI"],"kwargs":{"openai_api_key":{"lc":1,"type":"secret","id":["OPENAI_API_KEY"]},"model":"o3-mini"}}`
272+
);
273+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env node
2+
import "../dist/extract_serializable_fields.js";

libs/langchain-scripts/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
},
1515
"homepage": "https://github.com/langchain-ai/langchainjs/tree/main/libs/langchain-scripts/",
1616
"bin": {
17+
"extract_serializable_fields": "bin/extract_serializable_fields.js",
1718
"filter_spam_comment": "bin/filter_spam_comment.js",
1819
"lc_build": "bin/build.js",
1920
"notebook_validate": "bin/validate_notebook.js"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import ts from "typescript";
2+
import * as path from "path";
3+
4+
function extractConstructorParams(
5+
sourceFile: string,
6+
className: string
7+
): { type: string; fields: string[] } | null {
8+
const absolutePath = path.resolve(sourceFile);
9+
const program = ts.createProgram([absolutePath], {
10+
target: ts.ScriptTarget.ES2015,
11+
module: ts.ModuleKind.CommonJS,
12+
});
13+
const source = program.getSourceFile(absolutePath);
14+
const typeChecker = program.getTypeChecker();
15+
16+
if (!source) {
17+
console.error(`Could not find source file: ${absolutePath}`);
18+
return null;
19+
}
20+
21+
let result: { type: string; fields: string[] } | null = null;
22+
23+
function visit(node: ts.Node) {
24+
if (ts.isClassDeclaration(node) && node.name?.text === className) {
25+
node.members.forEach((member) => {
26+
if (
27+
ts.isConstructorDeclaration(member) &&
28+
member.parameters.length > 0
29+
) {
30+
const firstParam = member.parameters[0];
31+
const type = typeChecker.getTypeAtLocation(firstParam);
32+
const typeString = typeChecker.typeToString(type);
33+
34+
// Get properties of the type
35+
const fields: string[] = [];
36+
type.getProperties().forEach((prop) => {
37+
// Get the type of the property
38+
const propType = typeChecker.getTypeOfSymbolAtLocation(
39+
prop,
40+
firstParam
41+
);
42+
// Only include non-function properties that don't start with __
43+
if (
44+
!prop.getName().startsWith("__") &&
45+
prop.getName() !== "callbackManager" &&
46+
!(propType.getCallSignatures().length > 0)
47+
) {
48+
fields.push(prop.getName());
49+
}
50+
});
51+
52+
result = {
53+
type: typeString,
54+
fields,
55+
};
56+
}
57+
});
58+
}
59+
ts.forEachChild(node, visit);
60+
}
61+
62+
visit(source);
63+
return result;
64+
}
65+
const filepath = process.argv[2];
66+
const className = process.argv[3];
67+
68+
if (!filepath || !className) {
69+
console.error(
70+
"Usage: node extract_serializable_fields.ts <filepath> <className>"
71+
);
72+
process.exit(1);
73+
}
74+
75+
const results = extractConstructorParams(filepath, className);
76+
77+
if (results?.fields?.length) {
78+
console.log(JSON.stringify(results?.fields, null, 2));
79+
} else {
80+
console.error("No constructor parameters found");
81+
}

yarn.lock

+2-1
Original file line numberDiff line numberDiff line change
@@ -13035,7 +13035,7 @@ __metadata:
1303513035
zod: ^3.22.4
1303613036
zod-to-json-schema: ^3.22.3
1303713037
peerDependencies:
13038-
"@langchain/core": ">=0.3.29 <0.4.0"
13038+
"@langchain/core": ">=0.3.39 <0.4.0"
1303913039
languageName: unknown
1304013040
linkType: soft
1304113041

@@ -13200,6 +13200,7 @@ __metadata:
1320013200
tsx: ^4.16.2
1320113201
typescript: ^5.4.5
1320213202
bin:
13203+
extract_serializable_fields: bin/extract_serializable_fields.js
1320313204
filter_spam_comment: bin/filter_spam_comment.js
1320413205
lc_build: bin/build.js
1320513206
notebook_validate: bin/validate_notebook.js

0 commit comments

Comments
 (0)